1 /* 2 * JBoss, Home of Professional Open Source 3 * Copyright 2005, JBoss Inc., and individual contributors as indicated 4 * by the @authors tag. See the copyright.txt in the distribution for a 5 * full listing of individual contributors. 6 * 7 * This is free software; you can redistribute it and/or modify it 8 * under the terms of the GNU Lesser General Public License as 9 * published by the Free Software Foundation; either version 2.1 of 10 * the License, or (at your option) any later version. 11 * 12 * This software is distributed in the hope that it will be useful, 13 * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 * Lesser General Public License for more details. 16 * 17 * You should have received a copy of the GNU Lesser General Public 18 * License along with this software; if not, write to the Free 19 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 20 * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 21 */ 22 package org.jboss.security.auth.spi; 23 24 import java.io.IOException; 25 import java.lang.reflect.InvocationTargetException; 26 import java.lang.reflect.Method; 27 import java.security.Principal; 28 import java.util.HashMap; 29 import java.util.Map; 30 31 import javax.security.auth.Subject; 32 import javax.security.auth.callback.Callback; 33 import javax.security.auth.callback.CallbackHandler; 34 import javax.security.auth.callback.NameCallback; 35 import javax.security.auth.callback.PasswordCallback; 36 import javax.security.auth.callback.UnsupportedCallbackException; 37 import javax.security.auth.login.FailedLoginException; 38 import javax.security.auth.login.LoginException; 39 40 import org.jboss.crypto.digest.DigestCallback; 41 42 43 /** An abstract subclass of AbstractServerLoginModule that imposes 44 * an identity == String username, credentials == String password view on 45 * the login process. 46 * <p> 47 * Subclasses override the <code>getUsersPassword()</code> 48 * and <code>getRoleSets()</code> methods to return the expected password and roles 49 * for the user. 50 * 51 * @see #getUsername() 52 * @see #getUsersPassword() 53 * @see #getRoleSets() 54 * @see #createIdentity(String) 55 56 @author Scott.Stark@jboss.org 57 @version $Revision: 86122 $ 58 */ 59 public abstract class UsernamePasswordLoginModule extends AbstractServerLoginModule 60 { 61 /** The login identity */ 62 private Principal identity; 63 /** The proof of login identity */ 64 private char[] credential; 65 /** the message digest algorithm used to hash passwords. If null then 66 plain passwords will be used. */ 67 private String hashAlgorithm = null; 68 /** the name of the charset/encoding to use when converting the password 69 String to a byte array. Default is the platform's default encoding. 70 */ 71 private String hashCharset = null; 72 /** the string encoding format to use. Defaults to base64. */ 73 private String hashEncoding = null; 74 /** A flag indicating if the password comparison should ignore case */ 75 private boolean ignorePasswordCase; 76 /** A flag indicating if the store password should be hashed using the hashAlgorithm */ 77 private boolean hashStorePassword; 78 79 /** A flag indicating if the user supplied password should be hashed using the hashAlgorithm */ 80 private boolean hashUserPassword = true; 81 /** A flag that restores the ability to override the createPasswordHash(String,String) */ 82 private boolean legacyCreatePasswordHash; 83 84 /** A flag that indicates whether validation errors should be exposed to clients or not */ 85 private boolean throwValidateError = false; 86 /** A {@code Throwable} representing the validation error */ 87 private Throwable validateError; 88 89 /** The input validator instance used to validate the username and password supplied by the client. */ 90 private InputValidator inputValidator = null; 91 92 /** Override the superclass method to look for the following options after 93 first invoking the super version. 94 @param options : 95 option: hashAlgorithm - the message digest algorithm used to hash passwords. 96 If null then plain passwords will be used. 97 option: hashCharset - the name of the charset/encoding to use when converting 98 the password String to a byte array. Default is the platform's default 99 encoding. 100 option: hashEncoding - the string encoding format to use. Defaults to base64. 101 option: ignorePasswordCase: A flag indicating if the password comparison 102 should ignore case. 103 option: digestCallback - The class name of the DigestCallback {@link org.jboss.crypto.digest.DigestCallback} 104 implementation that includes pre/post digest content like salts for hashing 105 the input password. Only used if hashAlgorithm has been specified. 106 option: hashStorePassword - A flag indicating if the store password returned 107 from #getUsersPassword() should be hashed . 108 option: hashUserPassword - A flag indicating if the user entered password should be hashed. 109 option: storeDigestCallback - The class name of the DigestCallback {@link org.jboss.crypto.digest.DigestCallback} 110 implementation that includes pre/post digest content like salts for hashing 111 the store/expected password. Only used if hashStorePassword or hashUserPassword is true and 112 hashAlgorithm has been specified. 113 */ 114 @Override 115 public void initialize(Subject subject, CallbackHandler callbackHandler, 116 Map<String,?> sharedState, Map<String,?> options) 117 { 118 super.initialize(subject, callbackHandler, sharedState, options); 119 120 // Check to see if password hashing has been enabled. 121 // If an algorithm is set, check for a format and charset. 122 hashAlgorithm = (String) options.get("hashAlgorithm"); 123 if( hashAlgorithm != null ) 124 { 125 hashEncoding = (String) options.get("hashEncoding"); 126 if( hashEncoding == null ) 127 hashEncoding = Util.BASE64_ENCODING; 128 hashCharset = (String) options.get("hashCharset"); 129 if( log.isTraceEnabled() ) 130 { 131 log.trace("Password hashing activated: algorithm = " + hashAlgorithm 132 + ", encoding = " + hashEncoding 133 + ", charset = " + (hashCharset == null ? "{default}" : hashCharset) 134 + ", callback = " + options.get("digestCallback") 135 + ", storeCallback = " + options.get("storeDigestCallback") 136 ); 137 } 138 } 139 String flag = (String) options.get("ignorePasswordCase"); 140 ignorePasswordCase = Boolean.valueOf(flag).booleanValue(); 141 flag = (String) options.get("hashStorePassword"); 142 hashStorePassword = Boolean.valueOf(flag).booleanValue(); 143 flag = (String) options.get("hashUserPassword"); 144 if( flag != null ) 145 hashUserPassword = Boolean.valueOf(flag).booleanValue(); 146 flag = (String) options.get("legacyCreatePasswordHash"); 147 if( flag != null ) 148 legacyCreatePasswordHash = Boolean.valueOf(flag).booleanValue(); 149 flag = (String) options.get("throwValidateError"); 150 if(flag != null) 151 this.throwValidateError = Boolean.valueOf(flag).booleanValue(); 152 // instantiate the input validator class. 153 flag = (String) options.get("inputValidator"); 154 if(flag != null) 155 { 156 try 157 { 158 Class<?> validatorClass = SecurityActions.loadClass(flag); 159 this.inputValidator = (InputValidator) validatorClass.newInstance(); 160 } 161 catch(Exception e) 162 { 163 this.log.debug("Unable to instantiate input validator class: " + flag); 164 } 165 } 166 } 167 168 /** Perform the authentication of the username and password. 169 */ 170 @Override 171 @SuppressWarnings("unchecked") 172 public boolean login() throws LoginException 173 { 174 // See if shared credentials exist 175 if( super.login() == true ) 176 { 177 // Setup our view of the user 178 Object username = sharedState.get("javax.security.auth.login.name"); 179 if( username instanceof Principal ) 180 identity = (Principal) username; 181 else 182 { 183 String name = username.toString(); 184 try 185 { 186 identity = createIdentity(name); 187 } 188 catch(Exception e) 189 { 190 log.debug("Failed to create principal", e); 191 throw new LoginException("Failed to create principal: "+ e.getMessage()); 192 } 193 } 194 Object password = sharedState.get("javax.security.auth.login.password"); 195 if( password instanceof char[] ) 196 credential = (char[]) password; 197 else if( password != null ) 198 { 199 String tmp = password.toString(); 200 credential = tmp.toCharArray(); 201 } 202 return true; 203 } 204 205 super.loginOk = false; 206 String[] info = getUsernameAndPassword(); 207 String username = info[0]; 208 String password = info[1]; 209 210 // validate the retrieved username and password. 211 if(this.inputValidator != null) 212 { 213 try 214 { 215 this.inputValidator.validateUsernameAndPassword(username, password); 216 } 217 catch(InputValidationException ive) 218 { 219 throw new FailedLoginException(ive.getMessage()); 220 } 221 } 222 223 if( username == null && password == null ) 224 { 225 identity = unauthenticatedIdentity; 226 super.log.trace("Authenticating as unauthenticatedIdentity="+identity); 227 } 228 229 if( identity == null ) 230 { 231 try 232 { 233 identity = createIdentity(username); 234 } 235 catch(Exception e) 236 { 237 log.debug("Failed to create principal", e); 238 throw new LoginException("Failed to create principal: "+ e.getMessage()); 239 } 240 241 // Hash the user entered password if password hashing is in use 242 if( hashAlgorithm != null && hashUserPassword == true ) 243 password = createPasswordHash(username, password, "digestCallback"); 244 // Validate the password supplied by the subclass 245 String expectedPassword = getUsersPassword(); 246 // Allow the storeDigestCallback to hash the expected password 247 if( hashAlgorithm != null && hashStorePassword == true ) 248 expectedPassword = createPasswordHash(username, expectedPassword, "storeDigestCallback"); 249 if( validatePassword(password, expectedPassword) == false ) 250 { 251 Throwable ex = getValidateError(); 252 FailedLoginException fle = new FailedLoginException("Password Incorrect/Password Required"); 253 if( ex != null && this.throwValidateError == true) 254 { 255 log.debug("Bad password for username="+username, ex); 256 fle.initCause(ex); 257 } 258 else 259 { 260 log.debug("Bad password for username="+username); 261 } 262 throw fle; 263 } 264 } 265 266 if( getUseFirstPass() == true ) 267 { // Add the username and password to the shared state map 268 sharedState.put("javax.security.auth.login.name", username); 269 sharedState.put("javax.security.auth.login.password", credential); 270 } 271 super.loginOk = true; 272 super.log.trace("User '" + identity + "' authenticated, loginOk="+loginOk); 273 return true; 274 } 275 276 @Override 277 protected Principal getIdentity() 278 { 279 return identity; 280 } 281 @Override 282 protected Principal getUnauthenticatedIdentity() 283 { 284 return unauthenticatedIdentity; 285 } 286 287 protected Object getCredentials() 288 { 289 return credential; 290 } 291 protected String getUsername() 292 { 293 String username = null; 294 if( getIdentity() != null ) 295 username = getIdentity().getName(); 296 return username; 297 } 298 299 /** Called by login() to acquire the username and password strings for 300 authentication. This method does no validation of either. 301 @return String[], [0] = username, [1] = password 302 @exception LoginException thrown if CallbackHandler is not set or fails. 303 */ 304 protected String[] getUsernameAndPassword() throws LoginException 305 { 306 String[] info = {null, null}; 307 // prompt for a username and password 308 if( callbackHandler == null ) 309 { 310 throw new LoginException("Error: no CallbackHandler available " + 311 "to collect authentication information"); 312 } 313 314 NameCallback nc = new NameCallback("User name: ", "guest"); 315 PasswordCallback pc = new PasswordCallback("Password: ", false); 316 Callback[] callbacks = {nc, pc}; 317 String username = null; 318 String password = null; 319 try 320 { 321 callbackHandler.handle(callbacks); 322 username = nc.getName(); 323 char[] tmpPassword = pc.getPassword(); 324 if( tmpPassword != null ) 325 { 326 credential = new char[tmpPassword.length]; 327 System.arraycopy(tmpPassword, 0, credential, 0, tmpPassword.length); 328 pc.clearPassword(); 329 password = new String(credential); 330 } 331 } 332 catch(IOException e) 333 { 334 LoginException le = new LoginException("Failed to get username/password"); 335 le.initCause(e); 336 throw le; 337 } 338 catch(UnsupportedCallbackException e) 339 { 340 LoginException le = new LoginException("CallbackHandler does not support: " + e.getCallback()); 341 le.initCause(e); 342 throw le; 343 } 344 info[0] = username; 345 info[1] = password; 346 return info; 347 } 348 349 /** 350 * If hashing is enabled, this method is called from <code>login()</code> 351 * prior to password validation. 352 * <p> 353 * Subclasses may override it to provide customized password hashing, 354 * for example by adding user-specific information or salting. If the 355 * legacyCreatePasswordHash option is set, this method tries to delegate 356 * to the legacy createPasswordHash(String, String) method via reflection 357 * and this is the value returned. 358 * <p> 359 * The default version calculates the hash based on the following options: 360 * <ul> 361 * <li><em>hashAlgorithm</em>: The digest algorithm to use. 362 * <li><em>hashEncoding</em>: The format used to store the hashes (base64 or hex) 363 * <li><em>hashCharset</em>: The encoding used to convert the password to bytes 364 * for hashing. 365 * <li><em>digestCallback</em>: The class name of the 366 * org.jboss.security.auth.spi.DigestCallback implementation that includes 367 * pre/post digest content like salts. 368 * </ul> 369 * It will return null if the hash fails for any reason, which will in turn 370 * cause <code>validatePassword()</code> to fail. 371 * 372 * @param username ignored in default version 373 * @param password the password string to be hashed 374 * @param digestOption - the login module option name of the DigestCallback 375 * @throws SecurityException - thrown if there is a failure to load the 376 * digestOption DigestCallback 377 */ 378 @SuppressWarnings("unchecked") 379 protected String createPasswordHash(String username, String password, 380 String digestOption) 381 throws LoginException 382 { 383 // Support for 4.0.2 createPasswordHash(String, String) override 384 if( legacyCreatePasswordHash ) 385 { 386 try 387 { 388 // Try to invoke the subclass createPasswordHash(String, String) 389 Class<?>[] sig = {String.class, String.class}; 390 Method createPasswordHash = getClass().getMethod("createPasswordHash", sig); 391 Object[] args = {username, password}; 392 String passwordHash = (String) createPasswordHash.invoke(this, args); 393 return passwordHash; 394 } 395 catch (InvocationTargetException e) 396 { 397 LoginException le = new LoginException("Failed to delegate createPasswordHash"); 398 le.initCause(e.getTargetException()); 399 throw le; 400 } 401 catch(Exception e) 402 { 403 LoginException le = new LoginException("Failed to delegate createPasswordHash"); 404 le.initCause(e); 405 throw le; 406 } 407 } 408 409 DigestCallback callback = null; 410 String callbackClassName = (String) options.get(digestOption); 411 if( callbackClassName != null ) 412 { 413 try 414 { 415 ClassLoader loader = SecurityActions.getContextClassLoader(); 416 Class<?> callbackClass = loader.loadClass(callbackClassName); 417 callback = (DigestCallback) callbackClass.newInstance(); 418 if( log.isTraceEnabled() ) 419 log.trace("Created DigestCallback: "+callback); 420 } 421 catch (Exception e) 422 { 423 if( log.isTraceEnabled() ) 424 log.trace("Failed to load DigestCallback", e); 425 SecurityException ex = new SecurityException("Failed to load DigestCallback"); 426 ex.initCause(e); 427 throw ex; 428 } 429 Map<String,Object> tmp = new HashMap<String,Object>(); 430 tmp.putAll(options); 431 tmp.put("javax.security.auth.login.name", username); 432 tmp.put("javax.security.auth.login.password", password); 433 434 callback.init(tmp); 435 // Check for a callbacks 436 Callback[] callbacks = (Callback[]) tmp.get("callbacks"); 437 if( callbacks != null ) 438 { 439 try 440 { 441 callbackHandler.handle(callbacks); 442 } 443 catch(IOException e) 444 { 445 LoginException le = new LoginException(digestOption+" callback failed"); 446 le.initCause(e); 447 throw le; 448 } 449 catch(UnsupportedCallbackException e) 450 { 451 LoginException le = new LoginException(digestOption+" callback failed"); 452 le.initCause(e); 453 throw le; 454 } 455 } 456 } 457 String passwordHash = Util.createPasswordHash(hashAlgorithm, hashEncoding, 458 hashCharset, username, password, callback); 459 return passwordHash; 460 } 461 462 /** 463 * Get the error associated with the validatePassword failure 464 * @return the Throwable seen during validatePassword, null if no 465 * error occurred. 466 */ 467 protected Throwable getValidateError() 468 { 469 return validateError; 470 } 471 472 /** 473 * Set the error associated with the validatePassword failure 474 * @param validateError 475 */ 476 protected void setValidateError(Throwable validateError) 477 { 478 this.validateError = validateError; 479 } 480 481 /** A hook that allows subclasses to change the validation of the input 482 password against the expected password. This version checks that 483 neither inputPassword or expectedPassword are null that that 484 inputPassword.equals(expectedPassword) is true; 485 @return true if the inputPassword is valid, false otherwise. 486 */ 487 protected boolean validatePassword(String inputPassword, String expectedPassword) 488 { 489 if( inputPassword == null || expectedPassword == null ) 490 return false; 491 boolean valid = false; 492 if( ignorePasswordCase == true ) 493 valid = inputPassword.equalsIgnoreCase(expectedPassword); 494 else 495 valid = inputPassword.equals(expectedPassword); 496 return valid; 497 } 498 499 500 /** Get the expected password for the current username available via 501 the getUsername() method. This is called from within the login() 502 method after the CallbackHandler has returned the username and 503 candidate password. 504 @return the valid password String 505 */ 506 abstract protected String getUsersPassword() throws LoginException; 507 508 }