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 }