1 /* 2 * Copyright (c) 2004, 2008, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.jmx.remote.security; 27 28 import com.sun.jmx.mbeanserver.GetPropertyAction; 29 import com.sun.jmx.mbeanserver.Util; 30 import java.io.File; 31 import java.io.FilePermission; 32 import java.io.IOException; 33 import java.security.AccessControlException; 34 import java.security.AccessController; 35 import java.util.Arrays; 36 import java.util.Map; 37 38 import javax.security.auth.*; 39 import javax.security.auth.callback.*; 40 import javax.security.auth.login.*; 41 import javax.security.auth.spi.*; 42 import javax.management.remote.JMXPrincipal; 43 44 import com.sun.jmx.remote.util.ClassLogger; 45 import com.sun.jmx.remote.util.EnvHelp; 46 47 /** 48 * This {@link LoginModule} performs file-based authentication. 49 * 50 * <p> A supplied username and password is verified against the 51 * corresponding user credentials stored in a designated password file. 52 * If successful then a new {@link JMXPrincipal} is created with the 53 * user's name and it is associated with the current {@link Subject}. 54 * Such principals may be identified and granted management privileges in 55 * the access control file for JMX remote management or in a Java security 56 * policy. 57 * 58 * By default, the following password file is used: 59 * <pre> 60 * ${java.home}/conf/management/jmxremote.password 61 * </pre> 62 * A different password file can be specified via the <code>passwordFile</code> 63 * configuration option. 64 * 65 * <p> This module recognizes the following <code>Configuration</code> options: 66 * <dl> 67 * <dt> <code>passwordFile</code> </dt> 68 * <dd> the path to an alternative password file. It is used instead of 69 * the default password file.</dd> 70 * 71 * <dt> <code>useFirstPass</code> </dt> 72 * <dd> if <code>true</code>, this module retrieves the username and password 73 * from the module's shared state, using "javax.security.auth.login.name" 74 * and "javax.security.auth.login.password" as the respective keys. The 75 * retrieved values are used for authentication. If authentication fails, 76 * no attempt for a retry is made, and the failure is reported back to 77 * the calling application.</dd> 78 * 79 * <dt> <code>tryFirstPass</code> </dt> 80 * <dd> if <code>true</code>, this module retrieves the username and password 81 * from the module's shared state, using "javax.security.auth.login.name" 82 * and "javax.security.auth.login.password" as the respective keys. The 83 * retrieved values are used for authentication. If authentication fails, 84 * the module uses the CallbackHandler to retrieve a new username and 85 * password, and another attempt to authenticate is made. If the 86 * authentication fails, the failure is reported back to the calling 87 * application.</dd> 88 * 89 * <dt> <code>storePass</code> </dt> 90 * <dd> if <code>true</code>, this module stores the username and password 91 * obtained from the CallbackHandler in the module's shared state, using 92 * "javax.security.auth.login.name" and 93 * "javax.security.auth.login.password" as the respective keys. This is 94 * not performed if existing values already exist for the username and 95 * password in the shared state, or if authentication fails.</dd> 96 * 97 * <dt> <code>clearPass</code> </dt> 98 * <dd> if <code>true</code>, this module clears the username and password 99 * stored in the module's shared state after both phases of authentication 100 * (login and commit) have completed.</dd> 101 * 102 * <dt> <code>hashPassword</code> </dt> 103 * <dd> if <code>true</code>, this module replaces clear text passwords 104 * with its hash, if present </dd> 105 * 106 * </dl> 107 */ 108 public class FileLoginModule implements LoginModule { 109 110 private static final String PASSWORD_FILE_NAME = "jmxremote.password"; 111 112 // Location of the default password file 113 private static final String DEFAULT_PASSWORD_FILE_NAME = 114 AccessController.doPrivileged(new GetPropertyAction("java.home")) + 115 File.separatorChar + "conf" + 116 File.separatorChar + "management" + File.separatorChar + 117 PASSWORD_FILE_NAME; 118 119 // Key to retrieve the stored username 120 private static final String USERNAME_KEY = 121 "javax.security.auth.login.name"; 122 123 // Key to retrieve the stored password 124 private static final String PASSWORD_KEY = 125 "javax.security.auth.login.password"; 126 127 // Log messages 128 private static final ClassLogger logger = 129 new ClassLogger("javax.management.remote.misc", "FileLoginModule"); 130 131 // Configurable options 132 private boolean useFirstPass = false; 133 private boolean tryFirstPass = false; 134 private boolean storePass = false; 135 private boolean clearPass = false; 136 private boolean hashPassword = false; 137 138 // Authentication status 139 private boolean succeeded = false; 140 private boolean commitSucceeded = false; 141 142 // Supplied username and password 143 private String username; 144 private char[] password; 145 private JMXPrincipal user; 146 147 // Initial state 148 private Subject subject; 149 private CallbackHandler callbackHandler; 150 private Map<String, Object> sharedState; 151 private Map<String, ?> options; 152 private String passwordFile; 153 private String passwordFileDisplayName; 154 private boolean userSuppliedPasswordFile; 155 private boolean hasJavaHomePermission; 156 private HashedPasswordManager hashPwdMgr; 157 158 /** 159 * Initialize this <code>LoginModule</code>. 160 * 161 * @param subject the <code>Subject</code> to be authenticated. 162 * @param callbackHandler a <code>CallbackHandler</code> to acquire the 163 * user's name and password. 164 * @param sharedState shared <code>LoginModule</code> state. 165 * @param options options specified in the login 166 * <code>Configuration</code> for this particular 167 * <code>LoginModule</code>. 168 */ 169 public void initialize(Subject subject, CallbackHandler callbackHandler, 170 Map<String,?> sharedState, 171 Map<String,?> options) 172 { 173 174 this.subject = subject; 175 this.callbackHandler = callbackHandler; 176 this.sharedState = Util.cast(sharedState); 177 this.options = options; 178 179 // initialize any configured options 180 tryFirstPass = 181 "true".equalsIgnoreCase((String)options.get("tryFirstPass")); 182 useFirstPass = 183 "true".equalsIgnoreCase((String)options.get("useFirstPass")); 184 storePass = 185 "true".equalsIgnoreCase((String)options.get("storePass")); 186 clearPass = 187 "true".equalsIgnoreCase((String)options.get("clearPass")); 188 hashPassword = 189 "true".equalsIgnoreCase((String)options.get("hashPassword")); 190 191 passwordFile = (String)options.get("passwordFile"); 192 passwordFileDisplayName = passwordFile; 193 userSuppliedPasswordFile = true; 194 195 // set the location of the password file 196 if (passwordFile == null) { 197 passwordFile = DEFAULT_PASSWORD_FILE_NAME; 198 userSuppliedPasswordFile = false; 199 try { 200 System.getProperty("java.home"); 201 hasJavaHomePermission = true; 202 passwordFileDisplayName = passwordFile; 203 } catch (SecurityException e) { 204 hasJavaHomePermission = false; 205 passwordFileDisplayName = PASSWORD_FILE_NAME; 206 } 207 } 208 } 209 210 /** 211 * Begin user authentication (Authentication Phase 1). 212 * 213 * <p> Acquire the user's name and password and verify them against 214 * the corresponding credentials from the password file. 215 * 216 * @return true always, since this <code>LoginModule</code> 217 * should not be ignored. 218 * @exception FailedLoginException if the authentication fails. 219 * @exception LoginException if this <code>LoginModule</code> 220 * is unable to perform the authentication. 221 */ 222 public boolean login() throws LoginException { 223 224 try { 225 if(hashPwdMgr == null) { 226 hashPwdMgr = new HashedPasswordManager(passwordFile, hashPassword); 227 } else { 228 hashPwdMgr.loadPasswords(); 229 } 230 } catch (IOException ioe) { 231 LoginException le = new LoginException( 232 "Error: unable to load the password file: " + 233 passwordFileDisplayName); 234 throw EnvHelp.initCause(le, ioe); 235 } catch (SecurityException e) { 236 if (userSuppliedPasswordFile || hasJavaHomePermission) { 237 throw e; 238 } else { 239 final FilePermission fp = 240 new FilePermission(passwordFileDisplayName, "read"); 241 AccessControlException ace = new AccessControlException( 242 "access denied " + fp.toString()); 243 ace.setStackTrace(e.getStackTrace()); 244 throw ace; 245 } 246 } 247 248 if (logger.debugOn()) { 249 logger.debug("login", 250 "Using password file: " + passwordFileDisplayName); 251 } 252 253 // attempt the authentication 254 if (tryFirstPass) { 255 256 try { 257 // attempt the authentication by getting the 258 // username and password from shared state 259 attemptAuthentication(true); 260 261 // authentication succeeded 262 succeeded = true; 263 if (logger.debugOn()) { 264 logger.debug("login", 265 "Authentication using cached password has succeeded"); 266 } 267 return true; 268 269 } catch (LoginException le) { 270 // authentication failed -- try again below by prompting 271 cleanState(); 272 logger.debug("login", 273 "Authentication using cached password has failed"); 274 } 275 276 } else if (useFirstPass) { 277 278 try { 279 // attempt the authentication by getting the 280 // username and password from shared state 281 attemptAuthentication(true); 282 283 // authentication succeeded 284 succeeded = true; 285 if (logger.debugOn()) { 286 logger.debug("login", 287 "Authentication using cached password has succeeded"); 288 } 289 return true; 290 291 } catch (LoginException le) { 292 // authentication failed 293 cleanState(); 294 logger.debug("login", 295 "Authentication using cached password has failed"); 296 297 throw le; 298 } 299 } 300 301 if (logger.debugOn()) { 302 logger.debug("login", "Acquiring password"); 303 } 304 305 // attempt the authentication using the supplied username and password 306 try { 307 attemptAuthentication(false); 308 309 // authentication succeeded 310 succeeded = true; 311 if (logger.debugOn()) { 312 logger.debug("login", "Authentication has succeeded"); 313 } 314 return true; 315 316 } catch (LoginException le) { 317 cleanState(); 318 logger.debug("login", "Authentication has failed"); 319 320 throw le; 321 } 322 } 323 324 /** 325 * Complete user authentication (Authentication Phase 2). 326 * 327 * <p> This method is called if the LoginContext's 328 * overall authentication has succeeded 329 * (all the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL 330 * LoginModules have succeeded). 331 * 332 * <p> If this LoginModule's own authentication attempt 333 * succeeded (checked by retrieving the private state saved by the 334 * <code>login</code> method), then this method associates a 335 * <code>JMXPrincipal</code> with the <code>Subject</code> located in the 336 * <code>LoginModule</code>. If this LoginModule's own 337 * authentication attempted failed, then this method removes 338 * any state that was originally saved. 339 * 340 * @exception LoginException if the commit fails 341 * @return true if this LoginModule's own login and commit 342 * attempts succeeded, or false otherwise. 343 */ 344 public boolean commit() throws LoginException { 345 346 if (succeeded == false) { 347 return false; 348 } else { 349 if (subject.isReadOnly()) { 350 cleanState(); 351 throw new LoginException("Subject is read-only"); 352 } 353 // add Principals to the Subject 354 if (!subject.getPrincipals().contains(user)) { 355 subject.getPrincipals().add(user); 356 } 357 358 if (logger.debugOn()) { 359 logger.debug("commit", 360 "Authentication has completed successfully"); 361 } 362 } 363 // in any case, clean out state 364 cleanState(); 365 commitSucceeded = true; 366 return true; 367 } 368 369 /** 370 * Abort user authentication (Authentication Phase 2). 371 * 372 * <p> This method is called if the LoginContext's overall authentication 373 * failed (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL 374 * LoginModules did not succeed). 375 * 376 * <p> If this LoginModule's own authentication attempt 377 * succeeded (checked by retrieving the private state saved by the 378 * <code>login</code> and <code>commit</code> methods), 379 * then this method cleans up any state that was originally saved. 380 * 381 * @exception LoginException if the abort fails. 382 * @return false if this LoginModule's own login and/or commit attempts 383 * failed, and true otherwise. 384 */ 385 public boolean abort() throws LoginException { 386 387 if (logger.debugOn()) { 388 logger.debug("abort", 389 "Authentication has not completed successfully"); 390 } 391 392 if (succeeded == false) { 393 return false; 394 } else if (succeeded == true && commitSucceeded == false) { 395 396 // Clean out state 397 succeeded = false; 398 cleanState(); 399 user = null; 400 } else { 401 // overall authentication succeeded and commit succeeded, 402 // but someone else's commit failed 403 logout(); 404 } 405 return true; 406 } 407 408 /** 409 * Logout a user. 410 * 411 * <p> This method removes the Principals 412 * that were added by the <code>commit</code> method. 413 * 414 * @exception LoginException if the logout fails. 415 * @return true in all cases since this <code>LoginModule</code> 416 * should not be ignored. 417 */ 418 public boolean logout() throws LoginException { 419 if (subject.isReadOnly()) { 420 cleanState(); 421 throw new LoginException ("Subject is read-only"); 422 } 423 subject.getPrincipals().remove(user); 424 425 // clean out state 426 cleanState(); 427 succeeded = false; 428 commitSucceeded = false; 429 user = null; 430 431 if (logger.debugOn()) { 432 logger.debug("logout", "Subject is being logged out"); 433 } 434 435 return true; 436 } 437 438 /** 439 * Attempt authentication 440 * 441 * @param usePasswdFromSharedState a flag to tell this method whether 442 * to retrieve the password from the sharedState. 443 */ 444 @SuppressWarnings("unchecked") // sharedState used as Map<String,Object> 445 private void attemptAuthentication(boolean usePasswdFromSharedState) 446 throws LoginException { 447 448 // get the username and password 449 getUsernamePassword(usePasswdFromSharedState); 450 451 if (!hashPwdMgr.authenticate(username, new String(password))) { 452 // username not found or passwords do not match 453 if (logger.debugOn()) { 454 logger.debug("login", "Invalid username or password"); 455 } 456 throw new FailedLoginException("Invalid username or password"); 457 } 458 459 // Save the username and password in the shared state 460 // only if authentication succeeded 461 if (storePass && 462 !sharedState.containsKey(USERNAME_KEY) && 463 !sharedState.containsKey(PASSWORD_KEY)) { 464 sharedState.put(USERNAME_KEY, username); 465 sharedState.put(PASSWORD_KEY, password); 466 } 467 468 // Create a new user principal 469 user = new JMXPrincipal(username); 470 471 if (logger.debugOn()) { 472 logger.debug("login", 473 "User '" + username + "' successfully validated"); 474 } 475 } 476 477 /** 478 * Get the username and password. 479 * This method does not return any value. 480 * Instead, it sets global name and password variables. 481 * 482 * <p> Also note that this method will set the username and password 483 * values in the shared state in case subsequent LoginModules 484 * want to use them via use/tryFirstPass. 485 * 486 * @param usePasswdFromSharedState boolean that tells this method whether 487 * to retrieve the password from the sharedState. 488 */ 489 private void getUsernamePassword(boolean usePasswdFromSharedState) 490 throws LoginException { 491 492 if (usePasswdFromSharedState) { 493 // use the password saved by the first module in the stack 494 username = (String)sharedState.get(USERNAME_KEY); 495 password = (char[])sharedState.get(PASSWORD_KEY); 496 return; 497 } 498 499 // acquire username and password 500 if (callbackHandler == null) 501 throw new LoginException("Error: no CallbackHandler available " + 502 "to garner authentication information from the user"); 503 504 Callback[] callbacks = new Callback[2]; 505 callbacks[0] = new NameCallback("username"); 506 callbacks[1] = new PasswordCallback("password", false); 507 508 try { 509 callbackHandler.handle(callbacks); 510 username = ((NameCallback)callbacks[0]).getName(); 511 char[] tmpPassword = ((PasswordCallback)callbacks[1]).getPassword(); 512 password = new char[tmpPassword.length]; 513 System.arraycopy(tmpPassword, 0, 514 password, 0, tmpPassword.length); 515 ((PasswordCallback)callbacks[1]).clearPassword(); 516 517 } catch (IOException ioe) { 518 LoginException le = new LoginException(ioe.toString()); 519 throw EnvHelp.initCause(le, ioe); 520 } catch (UnsupportedCallbackException uce) { 521 LoginException le = new LoginException( 522 "Error: " + uce.getCallback().toString() + 523 " not available to garner authentication " + 524 "information from the user"); 525 throw EnvHelp.initCause(le, uce); 526 } 527 } 528 529 /** 530 * Clean out state because of a failed authentication attempt 531 */ 532 private void cleanState() { 533 username = null; 534 if (password != null) { 535 Arrays.fill(password, ' '); 536 password = null; 537 } 538 539 if (clearPass) { 540 sharedState.remove(USERNAME_KEY); 541 sharedState.remove(PASSWORD_KEY); 542 } 543 } 544 }