// $HeadURL$
// $Id$
//
// Copyright © 2006, 2010, 2011, 2012 by the President and Fellows of Harvard College.
//
// Screensaver is an open-source project developed by the ICCB-L and NSRB labs
// at Harvard Medical School. This software is distributed under the terms of
// the GNU General Public License.
package edu.harvard.med.screensaver.ui.arch.auth;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Map;
import javax.security.auth.Subject;
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.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import org.apache.log4j.Logger;
import edu.harvard.med.authentication.AuthenticationClient;
import edu.harvard.med.authentication.AuthenticationRequestException;
import edu.harvard.med.authentication.AuthenticationResponseException;
import edu.harvard.med.authentication.AuthenticationResult;
import edu.harvard.med.authentication.Credentials;
import edu.harvard.med.screensaver.db.GenericEntityDAO;
import edu.harvard.med.screensaver.model.users.AdministratorUser;
import edu.harvard.med.screensaver.model.users.ScreeningRoomUser;
import edu.harvard.med.screensaver.model.users.ScreensaverUser;
import edu.harvard.med.screensaver.model.users.ScreensaverUserRole;
import edu.harvard.med.screensaver.util.CryptoUtils;
/**
* This LoginModule authenticates users via one of two mechanisms, in the
* following order:
* <ol>
* <li>Via the login ID and password stored in the Screensaver database</li>
* <li>Via the injected AuthenticationClient.</li>
* </ol>
* <p>
* If user is successfully authenticated, a set of <code>Principal</code>s
* will be added to the login Subject, and which will be subsequently removed
* when the user is logged out. The Principals are obtained from a database, via
* a GenericEntityDAO object.
* <p>
* The LoginModule also allows administrators to login as normal users by
* specifying a composite login ID that is formed by concatenating admin's login
* ID with the user's login ID, separated by a colon. ("admin:user").
*/
// TODO: refactor into 3 separate LoginModules, one for normal login ID
// strategy, one for the AuthenticationClient strategy, and one for the
// composite admin:user login ID strategy. combine strategies via a
// ChainedLoginModule class.
public class ScreensaverLoginModule implements LoginModule
{
private static final Logger log = Logger.getLogger(ScreensaverLoginModule.class);
private static final String NO_SUCH_USER = "No such user";
private static final String NO_LOGIN_PRIVILEGES = "User does not have login privileges";
private static final String FOUND_SCREENSAVER_USER = "Found Screensaver user";
private static final String FOUND_ECOMMONS_USER = "Found eCommons user";
private GenericEntityDAO _dao;
private AuthenticationClient _authenticationClient;
// initial state
private Subject _subject;
private CallbackHandler _callbackHandler;
private Map _sharedState;
private Map _options;
// the authentication status
private AuthenticationResult _authenticationResult;
private boolean _isAuthenticated = false;
private boolean _commitSucceeded = false;
/**
* The Principals, which identify the user and the roles the Subject belongs
* to, and that were granted by this LoginModule (other LoginModules may grant
* the Subject other Principals, so the Subject's list of Principals may be a
* superset). By Tomcat's JAASRealm conventions, the first Principal must be
* the "user" Principal, while the others are "role" Principals.
*/
private ArrayList<Principal> _grantedPrincipals;
private ScreensaverUser _user;
// property getter and setter methods
public AuthenticationClient getAuthenticationClient()
{
return _authenticationClient;
}
public void setAuthenticationClient(AuthenticationClient authenticationClient)
{
_authenticationClient = authenticationClient;
}
public GenericEntityDAO getDao()
{
return _dao;
}
public void setDao(GenericEntityDAO dao)
{
_dao = dao;
}
// LoginModule interface methods
/**
* Initialize this <code>LoginModule</code>.
*
* @param subject the <code>Subject</code> to be authenticated.
* @param callbackHandler a <code>CallbackHandler</code> for communicating
* with the end user (prompting for user names and passwords, for
* example).
* @param sharedState shared <code>LoginModule</code> state.
* @param options _options specified in the login
* <code>Configuration</pcode> for this particular
* <code>LoginModule</code>.
*/
public void initialize(
Subject subject,
CallbackHandler callbackHandler,
Map sharedState,
Map options)
{
log.debug("initialize()");
_subject = subject;
_callbackHandler = callbackHandler;
_sharedState = sharedState;
_options = options;
_authenticationResult = null;
_isAuthenticated = false;
_commitSucceeded = false;
}
/**
* Authenticate the user by prompting for a user name and password.
*
* @return true in all cases since this <code>LoginModule</code> should not
* be ignored.
* @throws FailedLoginException if the authentication fails.
* @throws LoginException if this <code>LoginModule</code> is unable to
* perform the authentication.
*/
public boolean login() throws LoginException {
log.debug("login()");
// prompt for a user name 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("user name: ");
callbacks[1] = new PasswordCallback("password: ", false);
String username;
String switchToUsername = null;
char[] password;
try {
_callbackHandler.handle(callbacks);
// username and password
username = ((NameCallback) callbacks[0]).getName();
if (username.indexOf(':') > 0) {
String[] names = username.split(":");
username = names[0];
switchToUsername = names[1];
}
char[] tmpPassword = ((PasswordCallback) callbacks[1]).getPassword();
// treat a NULL password as an empty password
if (tmpPassword == null) {
tmpPassword = new char[0];
}
password = new char[tmpPassword.length];
System.arraycopy(tmpPassword, 0,
password, 0, tmpPassword.length);
((PasswordCallback)callbacks[1]).clearPassword();
} catch (java.io.IOException ioe) {
throw new LoginException(ioe.toString());
} catch (UnsupportedCallbackException uce) {
throw new LoginException("Error: " + uce.getCallback().toString() +
" not available to garner authentication information from the user");
}
log.debug("attempting authentication for user '" + username + "'");
//log.debug("user entered password: " + new String(password));
if (authenticateUser(username, password)) {
if (switchToUsername != null) {
switchUser(_user, switchToUsername);
}
return true;
}
return false;
}
private boolean authenticateUser(String username, char[] password) throws LoginException
{
// verify the username/password
try {
_user = findUserByLoginId(username);
if (_user != null) {
log.info(FOUND_SCREENSAVER_USER + " '" + username + "'");
verifyLoginPrivilege(username);
if (_user.getDigestedPassword().equals(CryptoUtils.digest(password))) {
_isAuthenticated = true;
_authenticationResult = new SimpleAuthenticationResult(username,
new String(password),
true,
1,
"success",
"user authenticated with native Screensaver account");
}
else {
_isAuthenticated = false;
_authenticationResult = new SimpleAuthenticationResult(username,
new String(password),
_isAuthenticated,
0,
"failure",
"user authentication failed for native Screensaver account");
}
}
else {
_user = findUserByECommonsId(username);
if (_user != null) {
log.info(FOUND_ECOMMONS_USER + " '" + _user.getECommonsId() + "'");
verifyLoginPrivilege(username);
_authenticationResult = _authenticationClient.authenticate(new Credentials(_user.getECommonsId(),
new String(password)));
_isAuthenticated = _authenticationResult.isAuthenticated();
}
else {
String message = NO_SUCH_USER + " '" + username + "'";
log.info(message);
throw new FailedLoginException(message);
}
}
if (_isAuthenticated) {
log.info("authentication succeeded for user '" + username +
"' with status code " + _authenticationResult.getStatusCode() +
" (" + _authenticationResult.getStatusCodeCategory() + ")");
return true;
}
else {
// authentication failed, clean out state
String statusMessage = _authenticationResult.getStatusMessage();
log.info("authentication failed for user '" + username +
"' with status code " + _authenticationResult.getStatusCode() +
" (" + _authenticationResult.getStatusCodeCategory() + ": '" + statusMessage + "')");
reset(true);
throw new FailedLoginException(statusMessage);
}
}
catch (AuthenticationRequestException e) {
log.error("error during login with authentication server request: " + e.getMessage());
throw new LoginException(e.getMessage());
}
catch (AuthenticationResponseException e) {
log.error("error during login with authentication server response: " + e.getMessage());
throw new LoginException(e.getMessage());
}
}
private void verifyLoginPrivilege(String username)
throws FailedLoginException
{
if (!_user.getScreensaverUserRoles().contains(ScreensaverUserRole.SCREENSAVER_USER)) {
String message = NO_LOGIN_PRIVILEGES + " '" + username + "'";
log.info(message);
throw new FailedLoginException(message);
}
}
private ScreensaverUser switchUser(ScreensaverUser user, String switchToECommonsId) throws LoginException {
if (!(user instanceof AdministratorUser && user.isUserInRole(ScreensaverUserRole.READ_EVERYTHING_ADMIN))) {
log.info("user " + user + " is not authorized to switch to another user");
}
else {
ScreensaverUser switchToUser = findUserByECommonsId(switchToECommonsId);
if (switchToUser == null) {
String msg = "cannot switch to user " + switchToECommonsId + ": no such user";
log.info(msg);
throw new LoginException(msg);
}
else if (!(switchToUser instanceof ScreeningRoomUser)) {
String msg = "switching to non-screening room user " + switchToUser + " is forbidden";
log.info(msg);
throw new LoginException(msg);
}
else if (!switchToUser.isUserInRole(ScreensaverUserRole.SCREENSAVER_USER)) {
String msg = "switching to user that does not have login privileges is forbidden: " + switchToUser;
log.info(msg);
throw new LoginException(msg);
}
else {
log.info("switching to screening room user " + switchToUser);
_user = switchToUser;
}
}
return _user;
}
/**
* This method is called if the LoginContext's overall authentication
* succeeded (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL
* LoginModules succeeded).
* <p>
* If this LoginModule's own authentication attempt succeeded (checked by
* retrieving the private state saved by the <code>login</code> method),
* then this method associates the user and role <code>Principal</code>s
* with the <code>Subject</code> located in the <code>LoginModule</code>.
* If this LoginModule's own authentication attempted failed, then this method
* removes any state that was originally saved.
*
* @throws 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 {
log.debug("commit()");
if (!_isAuthenticated) {
reset(true);
return false;
}
else {
// add Principals (authenticated identities) to the Subject
_grantedPrincipals = new ArrayList<Principal>();
_grantedPrincipals.add(new ScreensaverUserPrincipal(_user));
_grantedPrincipals.addAll(_user.getScreensaverUserRoles());
_subject.getPrincipals().addAll(_grantedPrincipals);
log.debug("authorized Subject with these Principals: " + _grantedPrincipals);
// in any case, clean out state
reset(false);
_commitSucceeded = true;
return true;
}
}
private ScreensaverUser findUserByLoginId(String username) {
return findUser(username, "loginId");
}
private ScreensaverUser findUserByECommonsId(String username) {
String normalizedUsername = username.toLowerCase();
if (!normalizedUsername.equals(username)) {
log.warn("lowercasing eCommons ID '" + username + "' to '" + normalizedUsername + "'");
}
return findUser(normalizedUsername, "ECommonsId");
}
/**
* @param userId the userId
* @param userIdField the field in {@link ScreensaverUser} entity bean:
* "loginId" or "eCommonsId" to lookup the userId in
* @return a {@link ScreensaverUser} or <code>null</code> if no user found
* with userId for given userIdField
*/
private ScreensaverUser findUser(final String userId, final String userIdField)
{
ScreensaverUser user = _dao.findEntityByProperty(ScreensaverUser.class,
userIdField,
userId,
true,
ScreensaverUser.roles);
if (user != null) {
log.debug("found user '" + userId + "' in database using field '" + userIdField +"'" );
}
else {
log.debug("no such user '" + userId + "' in database using field '" + userIdField +"'" );
}
return user;
}
/**
* This method is called if the LoginContext's overall authentication failed.
* (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules did
* not succeed).
* <p>
* If this LoginModule's own authentication attempt succeeded (checked by
* retrieving the private state saved by the <code>login</code> and
* <code>commit</code> methods), then this method cleans up any state that
* was originally saved.
*
* @throws 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 {
log.debug("abort()");
if (!_isAuthenticated) {
return false;
}
else if (_isAuthenticated && _commitSucceeded == false) {
// login succeeded but overall authentication failed
reset(true);
}
else {
// overall authentication succeeded and commit succeeded,
// but someone else's commit failed
logout();
}
return true;
}
/**
* Logout the user.
* <p>
* This method removes the <code>Principal</code>s that were added to the
* <code>Subject</code> by the <code>commit</code> method.
*
* @throws LoginException if the logout fails.
* @return true in all cases since this <code>LoginModule</code> should not
* be ignored.
*/
public boolean logout() throws LoginException
{
log.debug("logout()");
// remove the Principals from the Subject (i.e., authorizations begone!)
if (_grantedPrincipals != null) {
_subject.getPrincipals().removeAll(_grantedPrincipals);
}
reset(true);
return true;
}
// private methods
/**
* Reset internal state.
* @param alsoResetPrincipals resets the list of Principals that were granted to the subject
*/
private void reset(boolean alsoResetPrincipals)
{
// note: _subject must only be modified in initialize(); it can be inspected after logout, etc.
_authenticationResult = null;
_isAuthenticated = false;
if (alsoResetPrincipals) {
_grantedPrincipals = null;
}
}
}