View Javadoc
1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements. See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership. The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License. You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied. See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.wss4j.dom.validate;
21  
22  import java.io.IOException;
23  import java.nio.charset.StandardCharsets;
24  import java.security.MessageDigest;
25  
26  import javax.security.auth.callback.Callback;
27  import javax.security.auth.callback.UnsupportedCallbackException;
28  
29  import org.apache.wss4j.dom.WSConstants;
30  import org.apache.wss4j.common.ext.WSPasswordCallback;
31  import org.apache.wss4j.common.ext.WSSecurityException;
32  import org.apache.wss4j.common.util.UsernameTokenUtil;
33  import org.apache.wss4j.dom.handler.RequestData;
34  import org.apache.wss4j.dom.message.token.UsernameToken;
35  import org.apache.xml.security.utils.XMLUtils;
36  
37  /**
38   * This class validates a processed UsernameToken, extracted from the Credential passed to
39   * the validate method.
40   */
41  public class UsernameTokenValidator implements Validator {
42  
43      private static final org.slf4j.Logger LOG =
44          org.slf4j.LoggerFactory.getLogger(UsernameTokenValidator.class);
45  
46      /**
47       * Validate the credential argument. It must contain a non-null UsernameToken. A
48       * CallbackHandler implementation is also required to be set.
49       *
50       * If the password type is either digest or plaintext, it extracts a password from the
51       * CallbackHandler and then compares the passwords appropriately.
52       *
53       * If the password is null it queries a hook to allow the user to validate UsernameTokens
54       * of this type.
55       *
56       * @param credential the Credential to be validated
57       * @param data the RequestData associated with the request
58       * @throws WSSecurityException on a failed validation
59       */
60      public Credential validate(Credential credential, RequestData data) throws WSSecurityException {
61          if (credential == null || credential.getUsernametoken() == null) {
62              throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "noCredential");
63          }
64  
65          boolean handleCustomPasswordTypes = data.isHandleCustomPasswordTypes();
66          boolean passwordsAreEncoded = data.isEncodePasswords();
67          String requiredPasswordType = data.getRequiredPasswordType();
68  
69          UsernameToken usernameToken = credential.getUsernametoken();
70          usernameToken.setPasswordsAreEncoded(passwordsAreEncoded);
71  
72          String pwType = usernameToken.getPasswordType();
73          LOG.debug("UsernameToken user {}", usernameToken.getName());
74          LOG.debug("UsernameToken password type {}", pwType);
75  
76          if (requiredPasswordType != null && !requiredPasswordType.equals(pwType)) {
77              LOG.warn("Authentication failed as the received password type does not "
78                  + "match the required password type of: {}", requiredPasswordType);
79              throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION);
80          }
81  
82          //
83          // If the UsernameToken is hashed or plaintext, then retrieve the password from the
84          // callback handler and compare directly. If the UsernameToken is of some unknown type,
85          // then delegate authentication to the callback handler
86          //
87          String password = usernameToken.getPassword();
88          if (usernameToken.isHashed()) {
89              verifyDigestPassword(usernameToken, data);
90          } else if (WSConstants.PASSWORD_TEXT.equals(pwType)
91              || password != null && (pwType == null || pwType.trim().length() == 0)) {
92              verifyPlaintextPassword(usernameToken, data);
93          } else if (password != null) {
94              if (!handleCustomPasswordTypes) {
95                  LOG.warn("Authentication failed as handleCustomUsernameTokenTypes is false");
96                  throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION);
97              }
98              verifyCustomPassword(usernameToken, data);
99          } else {
100             verifyUnknownPassword(usernameToken, data);
101         }
102         return credential;
103     }
104 
105     /**
106      * Verify a UsernameToken containing a password of some unknown (but specified) password
107      * type. It does this by querying a CallbackHandler instance to obtain a password for the
108      * given username, and then comparing it against the received password.
109      * This method currently uses the same logic as the verifyPlaintextPassword case, but it in
110      * a separate protected method to allow users to override the validation of the custom
111      * password type specific case.
112      * @param usernameToken The UsernameToken instance to verify
113      * @throws WSSecurityException on a failed authentication.
114      */
115     protected void verifyCustomPassword(UsernameToken usernameToken,
116                                         RequestData data) throws WSSecurityException {
117         verifyPlaintextPassword(usernameToken, data);
118     }
119 
120     /**
121      * Verify a UsernameToken containing a plaintext password. It does this by querying a
122      * CallbackHandler instance to obtain a password for the given username, and then comparing
123      * it against the received password.
124      * This method currently uses the same logic as the verifyDigestPassword case, but it in
125      * a separate protected method to allow users to override the validation of the plaintext
126      * password specific case.
127      * @param usernameToken The UsernameToken instance to verify
128      * @throws WSSecurityException on a failed authentication.
129      */
130     protected void verifyPlaintextPassword(UsernameToken usernameToken,
131                                            RequestData data) throws WSSecurityException {
132         verifyDigestPassword(usernameToken, data);
133     }
134 
135     /**
136      * Verify a UsernameToken containing a password digest. It does this by querying a
137      * CallbackHandler instance to obtain a password for the given username, and then comparing
138      * it against the received password.
139      * @param usernameToken The UsernameToken instance to verify
140      * @throws WSSecurityException on a failed authentication.
141      */
142     protected void verifyDigestPassword(UsernameToken usernameToken,
143                                         RequestData data) throws WSSecurityException {
144         if (data.getCallbackHandler() == null) {
145             throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "noCallback");
146         }
147 
148         String user = usernameToken.getName();
149         String password = usernameToken.getPassword();
150         String nonce = usernameToken.getNonce();
151         String createdTime = usernameToken.getCreated();
152         String pwType = usernameToken.getPasswordType();
153         boolean passwordsAreEncoded = usernameToken.getPasswordsAreEncoded();
154 
155         WSPasswordCallback pwCb =
156             new WSPasswordCallback(user, null, pwType, WSPasswordCallback.USERNAME_TOKEN);
157         try {
158             data.getCallbackHandler().handle(new Callback[]{pwCb});
159         } catch (IOException | UnsupportedCallbackException e) {
160             LOG.debug(e.getMessage(), e);
161             throw new WSSecurityException(
162                 WSSecurityException.ErrorCode.FAILED_AUTHENTICATION, e
163             );
164         }
165         String origPassword = pwCb.getPassword();
166         if (origPassword == null) {
167             LOG.warn("Callback supplied no password for: {}", user);
168             throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION);
169         }
170         if (usernameToken.isHashed()) {
171             byte[] decodedNonce = XMLUtils.decode(nonce);
172             byte[] decodedPassword = XMLUtils.decode(password);
173             byte[] passDigest;
174             if (passwordsAreEncoded) {
175                 passDigest = UsernameTokenUtil.doRawPasswordDigest(decodedNonce, createdTime,
176                                                             XMLUtils.decode(origPassword));
177             } else {
178                 passDigest = UsernameTokenUtil.doRawPasswordDigest(decodedNonce, createdTime,
179                         origPassword.getBytes(StandardCharsets.UTF_8));
180             }
181             if (!MessageDigest.isEqual(decodedPassword, passDigest)) {
182                 throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION);
183             }
184         } else {
185             byte[] origPasswordBytes = origPassword.getBytes(StandardCharsets.UTF_8);
186             byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8);
187             if (!MessageDigest.isEqual(origPasswordBytes, passwordBytes)) {
188                 throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION);
189             }
190         }
191     }
192 
193     /**
194      * Verify a UsernameToken containing no password. An exception is thrown unless the user
195      * has explicitly allowed this use-case via WSHandlerConstants.ALLOW_USERNAMETOKEN_NOPASSWORD
196      * @param usernameToken The UsernameToken instance to verify
197      * @throws WSSecurityException on a failed authentication.
198      */
199     protected void verifyUnknownPassword(UsernameToken usernameToken,
200                                          RequestData data) throws WSSecurityException {
201 
202         boolean allowUsernameTokenDerivedKeys = data.isAllowUsernameTokenNoPassword();
203         if (!allowUsernameTokenDerivedKeys) {
204             LOG.warn("Authentication failed as the received UsernameToken does not "
205                 + "contain any password element");
206             throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION);
207         }
208     }
209 
210 }