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