/** * 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: *

* * @version $Revision: 1.4 $ $Date: 2005/05/26 19:46:05 $ $Author: gary $ * @see javax.security.auth.Subject * @see javax.security.auth.callback.CallbackHandler * @see javax.security.auth.spi.LoginModule * @since 12/04/01 */ public class XmlLoginModule implements LoginModule, ServiceClient, LoggerClient { // // Instance Variables // /** * The subject being authenticated. */ private Subject subject; /** * Provides the means for accessing user supplied data. */ private CallbackHandler callbackHandler; /** * Configuration options for this LoginModule. */ private Map options; /** * Map that contains state shared with other configured LoginModules. */ private Map sharedState; /** * Used by the LoginModule to lookup required Cams services. */ private ServiceFinder serviceFinder; /** * Used to the LoginModule to log security domain-specific messages. */ private Logger logger; /** * Flag that indicates if debug is enabled/disabled. */ private boolean debug; /** * Flag that indicates if user authentication succeeded. */ private boolean loginSucceeded; /** * A flag used to indicate login commit() transaction succeeded. */ private boolean commitSucceeded; /** * The username entered by the authenticating User. */ private String username; /** * The password entered by the authenticating User. */ private String password; /** * The XMLUserRepository the LoginModule authenticates against. */ private UserRepository userRepository; // // Constructor // /** * Default XmlLoginModule Constructor. */ public XmlLoginModule() { this.callbackHandler = null; this.commitSucceeded = false; this.debug = false; this.logger = StderrLogger.DEFAULT_STDERR_LOGGER; this.loginSucceeded = false; this.options = null; this.password = null; this.serviceFinder = null; this.sharedState = null; this.subject = null; this.username = null; this.userRepository = null; } // // Implements the ServiceClient interface // /** * Set the ServiceFinder. * * @param finder the class used by find Services by the ServiceClient. */ public void setServiceFinder(ServiceFinder finder) { this.serviceFinder = finder; } // // Implements the LoggerClient interface // /** * Sets the logger. * * @param logger logs messages */ public void setLogger(Logger logger) { this.logger = logger; } // // Implements the LoginModule interface // /** * Initialize this LoginModule. * *

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