/* * @(#)FileLoginModule.java 1.3 04/05/05 * * Copyright 2004 Sun Microsystems, Inc. All rights reserved. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. */ package com.sun.jmx.remote.security; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; 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; import sun.security.action.GetPropertyAction; /** * 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}/lib/management/jmxremote.password ** A different password file can be specified via the
passwordFile
* configuration option.
*
* This module recognizes the following Configuration
options:
*
passwordFile
useFirstPass
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
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
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
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.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 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: " + passwordFile);
throw (LoginException) 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: " + passwordFile);
}
// 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.
*/
private void attemptAuthentication(boolean usePasswdFromSharedState)
throws LoginException {
// get the username and password
getUsernamePassword(usePasswdFromSharedState);
String localPassword = null;
// 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 {
BufferedInputStream bis =
new BufferedInputStream(new FileInputStream(passwordFile));
userCredentials = new Properties();
userCredentials.load(bis);
bis.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 (LoginException) 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 (LoginException) 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); } } }