/** * Copyright (c) 1996-2006 Cafesoft, LLC. All Rights Reserved. * * This software is the confidential and proprietary information of * Cafesoft, LLC. ("Confidential Information"). You shall not disclose such * Confidential Information and shall use it only in accordance with the terms * of the license agreement you entered into with Cafesoft. * * CAFESOFT MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF THE * SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON- * INFRINGEMENT. CAFESOFT SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY * LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR * ITS DERIVATIVES. */ package com.cafesoft.cams.auth.login.module; import com.cafesoft.cams.auth.AuthenticationMethod; import com.cafesoft.cams.auth.CSRolePrincipal; import com.cafesoft.cams.auth.CSUserPrincipal; import com.cafesoft.cams.auth.login.userrepository.RepositoryUser; import com.cafesoft.cams.auth.login.userrepository.UserRepository; import com.cafesoft.cams.auth.login.userrepository.UserRepositoryException; import com.cafesoft.cams.service.UserRepositoryService; import com.cafesoft.core.log.Logger; import com.cafesoft.core.log.LoggerClient; import com.cafesoft.core.log.StderrLogger; import com.cafesoft.core.service.ServiceClient; import com.cafesoft.core.service.ServiceException; import com.cafesoft.core.service.ServiceFinder; import com.cafesoft.core.util.DigestString; import javax.security.auth.Subject; import javax.security.auth.login.FailedLoginException; import javax.security.auth.login.LoginException; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.auth.spi.LoginModule; import java.io.IOException; import java.security.Principal; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.StringTokenizer; /** * Implements the LoginModule interface found in JAAS to authenticate users * against a XML user repository. This particular implementation uses a * ServiceFinder object to retrieve the XML user repository to authenticate * against. *
* All configuration parameters are listed below: *
This method is called by the LoginContext
* after this LoginModule has been instantiated.
* The purpose of this method is to initialize this
* LoginModule with the relevant information.
* If this LoginModule does not understand
* any of the data stored in sharedState or
* options parameters, they can be ignored.
*
* @param subject the Subject to be authenticated.
* @param callbackHandler a CallbackHandler for communicating
* with the end user (prompting for usernames and
* passwords, for example).
* @param sharedState state shared with other configured LoginModules.
* @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.options = options;
this.sharedState = sharedState;
this.debug = "true".equals((String)options.get("debug"));
}
/**
* Method to authenticate a Subject (phase 1).
*
*
The implementation of this method authenticates
* a Subject. For example, it may prompt for
* Subject information such as a username and password
* and then attempt to verify the password. This method saves the
* result of the authentication attempt as private state within
* the LoginModule.
*
* @exception LoginException if the authentication fails
* @return true if the authentication succeeded, or false if this
* LoginModule should be ignored.
*/
public boolean login() throws LoginException
{
return authenticate();
}
/**
* Method to commit the authentication process (phase 2).
*
*
This method is called if the LoginContext's * overall authentication succeeded * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules * succeeded). * *
If this LoginModule's own authentication attempt
* succeeded (checked by retrieving the private state saved by the
* login method), then this method associates relevant
* Principals and Credentials with the Subject located in the
* LoginModule. If this LoginModule's own
* authentication attempted failed, then this method removes/destroys
* any state that was originally saved.
*
* @exception LoginException if the commit fails
* @return true if this method succeeded, or false if this
* LoginModule should be ignored.
*/
public boolean commit() throws LoginException
{
return associateRoles();
}
/**
* Method to abort the authentication process (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 method), then this method cleans up any state
* that was originally saved.
*
* @exception LoginException if the abort fails
* @return true if this method succeeded, or false if this
* LoginModule should be ignored.
*/
public boolean abort() throws LoginException
{
return abortAuthentication();
}
/**
* Method which logs out a Subject.
*
*
An implementation of this method might remove/destroy a Subject's
* Principals and Credentials.
*
* @exception LoginException if the logout fails
* @return true if this method succeeded, or false if this
* LoginModule should be ignored.
*/
public boolean logout() throws LoginException
{
return logoutUser();
}
//
// Private Methods
//
/**
* Authenticate the user.
*
* @return true if the authentication succeeds, false otherwise.
* @throws LoginException thrown if any error occurs while trying to
* authenticate the user.
* @throws FailedLoginException if authentication fails.
*/
private boolean authenticate()
throws LoginException, FailedLoginException
{
handleCallbacks();
// A username must always be supplied
if (username == null || username.length() == 0)
throw new FailedLoginException(
"[XmlLoginModule] Authentication failed");
// A non-empty password must be supplied unless empty
// passwords are permitted for this configuration.
String emptyPassword = (String)options.get("emptyPassword");
if ((emptyPassword == null) || ("false".equals(emptyPassword.trim())))
{
if ("".equals(this.password))
throw new FailedLoginException(
"[XmlLoginModule] Authentication failed");
}
else if (!("true".equals(emptyPassword.trim())))
{
throw new LoginException("[XmlLoginModule] " +
"Error: Option 'emptyPassword' must be 'true' or 'false'");
}
if(userRepository == null)
getUserRepository();
try
{
// Try to get the user
RepositoryUser ru = userRepository.getUser(username);
// If the user exists, get the user's password
if(ru != null)
{
String rp = ru.getPassword();
// The repository password may be "digested" and "salted" by use
// of a "Message Digest" algorithm, if so, then we'll need to
// digest and salt the user-entered password before comparison.
String digestString = password;
DigestString ds = new DigestString(rp);
// Is the password clear text (or does it have unknown
// algorithm)?
if(ds.isDigestString())
{
// Get the digestString from the user-entered password using
// the same ingredients found in the repository password;
// the password, the digest algorithm, and possibly "salt"
digestString = DigestString.createDigestString(password,
ds.getAlgorithm(), ds.getSalt(), ds.getLabel());
}
// Compare the cleartext or digested passwords
loginSucceeded = rp.equals(digestString);
}
else
{
if(debug)
{
// No user by that name
logger.debug(this, "[XmlLoginModule] User '" + username +
"' does not exist");
}
}
}
catch (NoSuchAlgorithmException e)
{
logger.error(this, "Unknown MessageDigestAlgorithm", e);
throw new LoginException("[XmlLoginModule] Error: " +
e.getMessage() + "\n");
}
catch(UserRepositoryException e)
{
logger.error(this, "Error accessing UserRepository", e);
throw new LoginException("[XmlLoginModule] Error: " +
e.getMessage() + "\n");
}
// Authentication failed, clean up state and throw FailedLoginException.
if(!loginSucceeded)
{
username = null;
password = null;
throw new FailedLoginException(
"[XmlLoginModule] Authentication failed");
}
return loginSucceeded;
}
/**
* Associate the roles with the user.
*
* @return true if roles successfully are assigned, false otherwise.
* @throws LoginException if an error occurs associating roles.
*/
private boolean associateRoles()
throws LoginException
{
// The login failed
if(!loginSucceeded)
return false;
if(userRepository == null)
getUserRepository();
List principalList = new ArrayList(5);
try
{
// Add default roles (if any) to list of roles
String defaultRoles = (String)options.get("defaultRoles");
if ((defaultRoles != null) && !("".equals(defaultRoles.trim())))
{
StringTokenizer st = new StringTokenizer(defaultRoles, ",");
while(st.hasMoreTokens())
principalList.add(
new CSRolePrincipal(st.nextElement().toString().trim()));
}
// Get the user and the user roles
RepositoryUser ru = userRepository.getUser(username);
String[] r = ru.getRoles();
// Add the user roles
if((r != null) && (r.length > 0))
{
for(int i = 0; i < r.length; i++)
{
// Add the CSRolePrincipal to the Subject
// only if it does not exist
Principal rolePrincipal = new CSRolePrincipal(r[i].trim());
if(!subject.getPrincipals(CSRolePrincipal.class).contains(rolePrincipal))
{
principalList.add(rolePrincipal);
if(debug)
{
logger.debug(this,
"[XmlLoginModule] Added CSRolePrincipal " +
r[i] + " to principal List for user '" +
username + "'");
}
}
else
{
if (debug)
{
logger.debug(this,
"[XmlLoginModule] CSRolePrincipal '" + r[i] +
"' already in Subject for '" + username +
"', not added to principal List");
}
}
}
}
}
catch (UserRepositoryException e)
{
logger.error(this, "Error in UserRepositoryService", e);
throw new LoginException("[XmlLoginModule] Error: " +
e.getMessage() + "\n");
}
// If commit gets this far, there is always a relevant CSUserPrincipal
// for this user, but only add it to the Subject if it does not exist
Principal userPrincipal = new CSUserPrincipal(username);
if (!subject.getPrincipals(CSUserPrincipal.class).contains(userPrincipal))
{
principalList.add(userPrincipal);
if (debug)
{
logger.debug(this, "[XmlLoginModule] Added CSUserPrincipal '" +
username + "' to principal List");
}
}
else
{
if (debug)
{
logger.debug(this, "[XmlLoginModule] CSUserPrincipal '" +
username + "' already in Subject, not added to principal List");
}
}
// Add all principals to subject
for (int i = 0; i < principalList.size(); i++)
subject.getPrincipals().add(principalList.get(i));
// Before we leave we must add to the Subject that the authentication
// method was of the password variety.
if(debug)
{
logger.debug(this, "[XmlLoginModule] Adding " +
AuthenticationMethod.PASSWORD.getName() +
" AuthenticationMethod");
}
subject.getPublicCredentials().add(
AuthenticationMethod.PASSWORD);
commitSucceeded = true;
// Clean up
username = null;
password = null;
return commitSucceeded;
}
/**
* Abort authentication attempt.
*
* @return true if abort succeeded, false otherwise.
* @throws LoginException if any error occurs logging out.
*/
private boolean abortAuthentication()
throws LoginException
{
// If authentication failed return false. If commit
// succeeded log the user out return true. Otherwise,
// Cleanup the login module and and return true.
if(!loginSucceeded)
return false;
if(commitSucceeded)
logout();
else
cleanup();
return true;
}
/**
* Log an authenticated user out.
*
* @return true if logout succeeded, false otherwise.
* @throws LoginException if an error occurs logging out.
*/
private boolean logoutUser()
throws LoginException
{
// Cleanup Object references
if(subject != null)
subject.getPrincipals().clear();
cleanup();
return true;
}
/**
* Clean up the resources of the LoginModule
*/
private void cleanup()
{
callbackHandler = null;
commitSucceeded = false;
debug = false;
logger = null;
loginSucceeded = false;
options = null;
password = null;
serviceFinder = null;
sharedState = null;
subject = null;
username = null;
userRepository = null;
}
/**
* Creates Callbacks and executes the defined CallbackHandlers handle
* method. In turn if successful this method will assign the username and
* password instance variables with their appropriate value.
*
* @throws LoginException if any error occurs executing the callbacks
*/
private void handleCallbacks()
throws LoginException
{
// Check for a callbackHandler
if(callbackHandler == null)
throw new LoginException(
"[XmlLoginModule] Error: no CallbackHandler available");
// This LoginModule requires a username and a password
Callback[] callbacks = new Callback[2];
callbacks[0] = new NameCallback("Xml username: ");
callbacks[1] = new PasswordCallback("Xml password: ", false);
try
{
// Prompt the user for a username and password
callbackHandler.handle(callbacks);
// Username must be supplied
this.username = ((NameCallback)callbacks[0]).getName();
// Treat a NULL password as an empty password
char[] tmpPassword = ((PasswordCallback)callbacks[1]).getPassword();
if(tmpPassword == null)
tmpPassword = new char[0];
// Save the password and clear the password callback
this.password = new String(tmpPassword);
((PasswordCallback)callbacks[1]).clearPassword();
}
catch(IOException e)
{
logger.error(this, "[XmlLoginModule] Login error occurred", e);
throw new LoginException("[XmlLoginModule] Error: " +
e.getMessage());
}
catch(UnsupportedCallbackException e)
{
logger.error(this,
"[XmlLoginModule] Login error occurred Callback not available",
e);
throw new LoginException("[XmlLoginModule] Error: " +
e.getMessage() + ", " + e.getCallback().toString() +
" not available");
}
if(debug)
logger.debug(this,
"[XmlLoginModule] User entered username = " + username);
}
/**
* Gets the XMLUserRepository for the LoginModule.
*
* @throws LoginException if an error occurs getting the
* UserRespository object.
*/
private void getUserRepository()
throws LoginException
{
try
{
if(serviceFinder == null)
throw new LoginException(
"[XmlLoginModule] Error: ServiceFinder is null");
String serviceId = (String)options.get("serviceId");
if((serviceId == null) || (serviceId.trim().length() == 0))
throw new LoginException("[XmlLoginModule] Error: Required " +
"'serviceId' property missing");
UserRepositoryService urs = (UserRepositoryService)
serviceFinder.find(serviceId, UserRepositoryService.class);
userRepository = urs.getUserRepository();
}
catch(ServiceException e)
{
logger.error(this, "Error finding UserRepositoryService", e);
throw new LoginException("[XmlLoginModule] Error: " +
e.getMessage());
}
}
} // End of class : XmlLoginModule