/* * Copyright (c) 2004, 2008, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.jmx.remote.security; import com.sun.jmx.mbeanserver.GetPropertyAction; import com.sun.jmx.mbeanserver.Util; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FilePermission; import java.io.IOException; import java.security.AccessControlException; import java.security.AccessController; import java.util.Arrays; import java.util.Hashtable; import java.util.Map; import java.util.Properties; import javax.security.auth.*; import javax.security.auth.callback.*; import javax.security.auth.login.*; import javax.security.auth.spi.*; import javax.management.remote.JMXPrincipal; import com.sun.jmx.remote.util.ClassLogger; import com.sun.jmx.remote.util.EnvHelp; import sun.management.jmxremote.ConnectorBootstrap; /** * This {@link LoginModule} performs file-based authentication. * *

A supplied username and password is verified against the * corresponding user credentials stored in a designated password file. * If successful then a new {@link JMXPrincipal} is created with the * user's name and it is associated with the current {@link Subject}. * Such principals may be identified and granted management privileges in * the access control file for JMX remote management or in a Java security * policy. * *

The password file comprises a list of key-value pairs as specified in * {@link Properties}. The key represents a user's name and the value is its * associated cleartext password. By default, the following password file is * used: *

 *     ${java.home}/conf/management/jmxremote.password
 * 
* A different password file can be specified via the passwordFile * configuration option. * *

This module recognizes the following Configuration options: *

*
passwordFile
*
the path to an alternative password file. It is used instead of * the default password file.
* *
useFirstPass
*
if true, this module retrieves the username and password * from the module's shared state, using "javax.security.auth.login.name" * and "javax.security.auth.login.password" as the respective keys. The * retrieved values are used for authentication. If authentication fails, * no attempt for a retry is made, and the failure is reported back to * the calling application.
* *
tryFirstPass
*
if true, this module retrieves the username and password * from the module's shared state, using "javax.security.auth.login.name" * and "javax.security.auth.login.password" as the respective keys. The * retrieved values are used for authentication. If authentication fails, * the module uses the CallbackHandler to retrieve a new username and * password, and another attempt to authenticate is made. If the * authentication fails, the failure is reported back to the calling * application.
* *
storePass
*
if true, this module stores the username and password * obtained from the CallbackHandler in the module's shared state, using * "javax.security.auth.login.name" and * "javax.security.auth.login.password" as the respective keys. This is * not performed if existing values already exist for the username and * password in the shared state, or if authentication fails.
* *
clearPass
*
if true, this module clears the username and password * stored in the module's shared state after both phases of authentication * (login and commit) have completed.
*
*/ public class FileLoginModule implements LoginModule { // Location of the default password file private static final String DEFAULT_PASSWORD_FILE_NAME = AccessController.doPrivileged(new GetPropertyAction("java.home")) + File.separatorChar + "conf" + File.separatorChar + "management" + File.separatorChar + ConnectorBootstrap.DefaultValues.PASSWORD_FILE_NAME; // Key to retrieve the stored username private static final String USERNAME_KEY = "javax.security.auth.login.name"; // Key to retrieve the stored password private static final String PASSWORD_KEY = "javax.security.auth.login.password"; // Log messages private static final ClassLogger logger = new ClassLogger("javax.management.remote.misc", "FileLoginModule"); // Configurable options private boolean useFirstPass = false; private boolean tryFirstPass = false; private boolean storePass = false; private boolean clearPass = false; // Authentication status private boolean succeeded = false; private boolean commitSucceeded = false; // Supplied username and password private String username; private char[] password; private JMXPrincipal user; // Initial state private Subject subject; private CallbackHandler callbackHandler; private Map sharedState; private Map options; private String passwordFile; private String passwordFileDisplayName; private boolean userSuppliedPasswordFile; private boolean hasJavaHomePermission; private Properties userCredentials; /** * Initialize this LoginModule. * * @param subject the Subject to be authenticated. * @param callbackHandler a CallbackHandler to acquire the * user's name and password. * @param sharedState shared LoginModule state. * @param options options specified in the login * Configuration for this particular * LoginModule. */ public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { this.subject = subject; this.callbackHandler = callbackHandler; this.sharedState = Util.cast(sharedState); this.options = options; // initialize any configured options tryFirstPass = "true".equalsIgnoreCase((String)options.get("tryFirstPass")); useFirstPass = "true".equalsIgnoreCase((String)options.get("useFirstPass")); storePass = "true".equalsIgnoreCase((String)options.get("storePass")); clearPass = "true".equalsIgnoreCase((String)options.get("clearPass")); passwordFile = (String)options.get("passwordFile"); passwordFileDisplayName = passwordFile; userSuppliedPasswordFile = true; // set the location of the password file if (passwordFile == null) { passwordFile = DEFAULT_PASSWORD_FILE_NAME; userSuppliedPasswordFile = false; try { System.getProperty("java.home"); hasJavaHomePermission = true; passwordFileDisplayName = passwordFile; } catch (SecurityException e) { hasJavaHomePermission = false; passwordFileDisplayName = ConnectorBootstrap.DefaultValues.PASSWORD_FILE_NAME; } } } /** * Begin user authentication (Authentication Phase 1). * *

Acquire the user's name and password and verify them against * the corresponding credentials from the password file. * * @return true always, since this LoginModule * should not be ignored. * @exception FailedLoginException if the authentication fails. * @exception LoginException if this LoginModule * is unable to perform the authentication. */ public boolean login() throws LoginException { try { loadPasswordFile(); } catch (IOException ioe) { LoginException le = new LoginException( "Error: unable to load the password file: " + passwordFileDisplayName); throw EnvHelp.initCause(le, ioe); } if (userCredentials == null) { throw new LoginException ("Error: unable to locate the users' credentials."); } if (logger.debugOn()) { logger.debug("login", "Using password file: " + passwordFileDisplayName); } // attempt the authentication if (tryFirstPass) { try { // attempt the authentication by getting the // username and password from shared state attemptAuthentication(true); // authentication succeeded succeeded = true; if (logger.debugOn()) { logger.debug("login", "Authentication using cached password has succeeded"); } return true; } catch (LoginException le) { // authentication failed -- try again below by prompting cleanState(); logger.debug("login", "Authentication using cached password has failed"); } } else if (useFirstPass) { try { // attempt the authentication by getting the // username and password from shared state attemptAuthentication(true); // authentication succeeded succeeded = true; if (logger.debugOn()) { logger.debug("login", "Authentication using cached password has succeeded"); } return true; } catch (LoginException le) { // authentication failed cleanState(); logger.debug("login", "Authentication using cached password has failed"); throw le; } } if (logger.debugOn()) { logger.debug("login", "Acquiring password"); } // attempt the authentication using the supplied username and password try { attemptAuthentication(false); // authentication succeeded succeeded = true; if (logger.debugOn()) { logger.debug("login", "Authentication has succeeded"); } return true; } catch (LoginException le) { cleanState(); logger.debug("login", "Authentication has failed"); throw le; } } /** * Complete user authentication (Authentication Phase 2). * *

This method is called if the LoginContext's * overall authentication has succeeded * (all the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL * LoginModules have succeeded). * *

If this LoginModule's own authentication attempt * succeeded (checked by retrieving the private state saved by the * login method), then this method associates a * JMXPrincipal with the Subject located in the * LoginModule. If this LoginModule's own * authentication attempted failed, then this method removes * any state that was originally saved. * * @exception LoginException if the commit fails * @return true if this LoginModule's own login and commit * attempts succeeded, or false otherwise. */ public boolean commit() throws LoginException { if (succeeded == false) { return false; } else { if (subject.isReadOnly()) { cleanState(); throw new LoginException("Subject is read-only"); } // add Principals to the Subject if (!subject.getPrincipals().contains(user)) { subject.getPrincipals().add(user); } if (logger.debugOn()) { logger.debug("commit", "Authentication has completed successfully"); } } // in any case, clean out state cleanState(); commitSucceeded = true; return true; } /** * Abort user authentication (Authentication Phase 2). * *

This method is called if the LoginContext's overall authentication * failed (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL * LoginModules did not succeed). * *

If this LoginModule's own authentication attempt * succeeded (checked by retrieving the private state saved by the * login and commit methods), * then this method cleans up any state that was originally saved. * * @exception LoginException if the abort fails. * @return false if this LoginModule's own login and/or commit attempts * failed, and true otherwise. */ public boolean abort() throws LoginException { if (logger.debugOn()) { logger.debug("abort", "Authentication has not completed successfully"); } if (succeeded == false) { return false; } else if (succeeded == true && commitSucceeded == false) { // Clean out state succeeded = false; cleanState(); user = null; } else { // overall authentication succeeded and commit succeeded, // but someone else's commit failed logout(); } return true; } /** * Logout a user. * *

This method removes the Principals * that were added by the commit method. * * @exception LoginException if the logout fails. * @return true in all cases since this LoginModule * should not be ignored. */ public boolean logout() throws LoginException { if (subject.isReadOnly()) { cleanState(); throw new LoginException ("Subject is read-only"); } subject.getPrincipals().remove(user); // clean out state cleanState(); succeeded = false; commitSucceeded = false; user = null; if (logger.debugOn()) { logger.debug("logout", "Subject is being logged out"); } return true; } /** * Attempt authentication * * @param usePasswdFromSharedState a flag to tell this method whether * to retrieve the password from the sharedState. */ @SuppressWarnings("unchecked") // sharedState used as Map private void attemptAuthentication(boolean usePasswdFromSharedState) throws LoginException { // get the username and password getUsernamePassword(usePasswdFromSharedState); String localPassword; // userCredentials is initialized in login() if (((localPassword = userCredentials.getProperty(username)) == null) || (! localPassword.equals(new String(password)))) { // username not found or passwords do not match if (logger.debugOn()) { logger.debug("login", "Invalid username or password"); } throw new FailedLoginException("Invalid username or password"); } // Save the username and password in the shared state // only if authentication succeeded if (storePass && !sharedState.containsKey(USERNAME_KEY) && !sharedState.containsKey(PASSWORD_KEY)) { sharedState.put(USERNAME_KEY, username); sharedState.put(PASSWORD_KEY, password); } // Create a new user principal user = new JMXPrincipal(username); if (logger.debugOn()) { logger.debug("login", "User '" + username + "' successfully validated"); } } /* * Read the password file. */ private void loadPasswordFile() throws IOException { FileInputStream fis; try { fis = new FileInputStream(passwordFile); } catch (SecurityException e) { if (userSuppliedPasswordFile || hasJavaHomePermission) { throw e; } else { final FilePermission fp = new FilePermission(passwordFileDisplayName, "read"); AccessControlException ace = new AccessControlException( "access denied " + fp.toString()); ace.setStackTrace(e.getStackTrace()); throw ace; } } try { final BufferedInputStream bis = new BufferedInputStream(fis); try { userCredentials = new Properties(); userCredentials.load(bis); } finally { bis.close(); } } finally { fis.close(); } } /** * Get the username and password. * This method does not return any value. * Instead, it sets global name and password variables. * *

Also note that this method will set the username and password * values in the shared state in case subsequent LoginModules * want to use them via use/tryFirstPass. * * @param usePasswdFromSharedState boolean that tells this method whether * to retrieve the password from the sharedState. */ private void getUsernamePassword(boolean usePasswdFromSharedState) throws LoginException { if (usePasswdFromSharedState) { // use the password saved by the first module in the stack username = (String)sharedState.get(USERNAME_KEY); password = (char[])sharedState.get(PASSWORD_KEY); return; } // acquire username and password if (callbackHandler == null) throw new LoginException("Error: no CallbackHandler available " + "to garner authentication information from the user"); Callback[] callbacks = new Callback[2]; callbacks[0] = new NameCallback("username"); callbacks[1] = new PasswordCallback("password", false); try { callbackHandler.handle(callbacks); username = ((NameCallback)callbacks[0]).getName(); char[] tmpPassword = ((PasswordCallback)callbacks[1]).getPassword(); password = new char[tmpPassword.length]; System.arraycopy(tmpPassword, 0, password, 0, tmpPassword.length); ((PasswordCallback)callbacks[1]).clearPassword(); } catch (IOException ioe) { LoginException le = new LoginException(ioe.toString()); throw EnvHelp.initCause(le, ioe); } catch (UnsupportedCallbackException uce) { LoginException le = new LoginException( "Error: " + uce.getCallback().toString() + " not available to garner authentication " + "information from the user"); throw EnvHelp.initCause(le, uce); } } /** * Clean out state because of a failed authentication attempt */ private void cleanState() { username = null; if (password != null) { Arrays.fill(password, ' '); password = null; } if (clearPass) { sharedState.remove(USERNAME_KEY); sharedState.remove(PASSWORD_KEY); } } }