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 }