/********************************************************************
 * Copyright © 2004 VHA. All rights reserved
 ********************************************************************/
package gov.va.med.fw.security.jaas;

import java.io.IOException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;

/**
 * Abstract class to provide LoginModule for applications that require only
 * username and password to authenticate. Subclass of this module must implement
 * authenticate method to perform the actual authentication.
 *
 * <p>
 * This LoginModule also recognizes the following <code>Configuration</code>
 * options:
 *
 * <pre>
 *     debug          if, true, debug messages are output to System.out.
 *
 *     useFirstPass   if, true, this LoginModule retrieves the
 *                    username and password from the module's shared state,
 *                    using &quot;javax.security.auth.login.name&quot; and
 *                    &quot;javax.security.auth.login.password&quot; 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 LoginModule retrieves the
 *                    the username and password from the module's shared state,
 *                    using &quot;javax.security.auth.login.name&quot; and
 *                    &quot;javax.security.auth.login.password&quot; 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 LoginModule stores the username and password
 *                    obtained from the CallbackHandler in the module's
 *                    shared state, using &quot;javax.security.auth.login.name&quot; and
 *                    &quot;javax.security.auth.login.password&quot; 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 LoginModule clears the username and password
 * 					 stored in the module's shared state after both phases of
 * 					 authentication (login and commit) have completed.
 * </pre>
 *
 * @author DNS   mansog
 * @date Mar 22, 2005
 */
public abstract class AbstractUserPasswordLoginModule extends
        AbstractLoginModule {
    //constants for storing username and password in shared state.
    private static final String LOGIN_NAME = "javax.security.auth.login.name";
    private static final String LOGIN_PASSWORD = "javax.security.auth.login.password";

    private String username = null;
    private char password[] = null;

    private List principalsList = null;
    private List pendingPrincipalsList = null;
    private boolean commitSucceeded = false;
    private boolean succeeded = false;

    /**
     * Authenticate the user given the username and password. This method
     * returns a list of <code>java.security.Principal</code> Objects on
     * successfull authentication. This method must return atleast one Principal
     * in the list.
     *
     * @param username
     * @param password
     * @return a list of java.security.Principal objects. Never null.
     * @throws LoginException
     *             if login failed.
     */
    protected abstract List authenticate(String username, char password[])
            throws LoginException;

    /*
     * (non-Javadoc)
     *
     * @see javax.security.auth.spi.LoginModule#login()
     */
    public boolean login() throws LoginException {
        if (isTryFirstPass()) {
            try {
                //Get the username and password from shared state
                attemptAuthentication(true);
                succeeded = true;
                debugIfOn("tryFirstPass succeeded");
                return true;
            } catch (LoginException ex) {
                cleanState();
                debugIfOn("tryFirstPass failed. Reason:" + ex.toString());
            }
        } else if (isUseFirstPass()) {
            try {
                //Get the username and password from shared state
                attemptAuthentication(true);
                succeeded = true;
                debugIfOn("useFirstPass succeeded");
                return true;
            } catch (LoginException ex) {
                cleanState();
                debugIfOn("useFirstPass failed");
                throw ex;
            }
        }

        //both tryFirstPass and useFirstPass did not succeed or not enabled.
        // try using the callbackhandler
        try {
            attemptAuthentication(false);
            succeeded = true;
            debugIfOn("regular authentication succeeded for user "+username);
            return true;
        } catch (LoginException ex) {
            debugIfOn("regular authentication failed for user "+username);
            cleanState();
            throw ex;
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.security.auth.spi.LoginModule#commit()
     */
    public boolean commit() throws LoginException {
        if (!succeeded || pendingPrincipalsList == null) {
            return false;
        } else if (getSubject().isReadOnly()) {
            cleanState();
            throw new LoginException("Subject is Readonly");
        }
        principalsList = new ArrayList();
        Set principals = subject.getPrincipals();

        for (int p = 0; p < pendingPrincipalsList.size(); p++) {
            Principal principal = (Principal) pendingPrincipalsList.get(p);
            if (!principals.contains(principal)) {
                principals.add(principal);
            }
            //also add in principals List
            principalsList.add(principal);
        }
        cleanState();
        commitSucceeded = true;
        return true;
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.security.auth.spi.LoginModule#abort()
     */
    public boolean abort() throws LoginException {
        debugIfOn("abort for user "+username);
        if (succeeded == false || pendingPrincipalsList == null) {
            return false;
        } else if (succeeded == true && pendingPrincipalsList != null
                && !commitSucceeded) {
            succeeded = false;
            pendingPrincipalsList = null;
            cleanState();
        } else {
            // overall authentication succeeded and commit succeeded,
            // but someone else's commit failed
            logout();
        }
        debugIfOn("aborted authentication for user "+username);
        return true;
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.security.auth.spi.LoginModule#logout()
     */
    public boolean logout() throws LoginException {
        debugIfOn("logout for user "+username);
        if (subject.isReadOnly()) {
            cleanState();
            throw new LoginException("Subject is Readonly");
        }
        //remove the principals that we added.
        Set principals = subject.getPrincipals();
        for (int p = 0; p < principalsList.size(); p++) {
            principals.remove(principalsList.get(p));
        }

        pendingPrincipalsList = null;
        principalsList = null;
        succeeded = false;
        commitSucceeded = false;
        debugIfOn("user "+username+" logged out.");

        return true;
    }

    /**
     * Attempt authentication using the shared state or callback handler.
     *
     * If authentication is successful, this method will also set the username
     * and password values in the shared state in case subsequent LoginModules
     * want to use them via use/tryFirstPass.
     *
     * @param fromSharedState
     *            boolean that tells this method whether to retrieve the
     *            password from the sharedState or not.
     */
    private void attemptAuthentication(boolean fromSharedState)
            throws LoginException {
        //set the global username and password before try to authenticate.
        setUsernameAndPassword(fromSharedState);
        debugIfOn("Trying to authenticate user "+username);
        //delegate the actual authentication to authenticate method
        pendingPrincipalsList = authenticate(username, password);
        //On success, store the username and password in shared state
        // (if storePass=true)
        storePasswordIfRequired(username, password);
    }

    /**
     * Stores the username and password values in the shared state if storePass
     * option is true.
     *
     * @param username
     * @param password
     */
    private void storePasswordIfRequired(String username, char[] password) {
        if (isStorePass() && !getSharedState().containsKey(LOGIN_NAME)
                && !getSharedState().containsKey(LOGIN_PASSWORD)) {
            debugIfOn("Storing username and password in shared state.");
            sharedState.put(LOGIN_NAME, username);
            sharedState.put(LOGIN_PASSWORD, password);
        }
    }

    /**
     * Get the username and password. This method does not return any value.
     * Instead, it sets global name and password variables.
     *
     * <p>
     *
     * @param fromSharedState
     *            boolean that tells this method whether to retrieve the
     *            password from the sharedState.
     */
    private void setUsernameAndPassword(boolean fromSharedState)
            throws LoginException {

        if (fromSharedState) {
            debugIfOn("Loading username and password information from shared state.");
            username = (String) getSharedState().get(LOGIN_NAME);
            password = (char[]) getSharedState().get(LOGIN_PASSWORD);
            return;
        }

        //if not from shared state,
        // get the username, password using callback handler.
        if (callbackHandler == null)
            throw new LoginException("Missing CallbackHandler.");

        debugIfOn("Using CallbackHandler to get the username and password.");

        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();
            password = ((PasswordCallback) callbacks[1]).getPassword();
            ((PasswordCallback) callbacks[1]).clearPassword();
        } catch (IOException ex) {
            throw new LoginException("Exception while getting user information from callback handler. "+ex.toString());
        } catch (UnsupportedCallbackException ex) {
            throw new LoginException("Unsupported CallbackHandler "
                    + ex.getCallback().toString());
        }
    }

    /**
     * Clean out state because of a failed authentication.
     */
    private void cleanState() {
        username = null;
        if (password != null) {
        	Arrays.fill(password, ' ');
            password = null;
        }
        if (isClearPass()) {
            debugIfOn("Removing username/password from shared state.");
            sharedState.remove(LOGIN_NAME);
            sharedState.remove(LOGIN_PASSWORD);
        }
    }

    /**
     * Helper log method.
     * @param msg
     */
    protected void debugIfOn(String msg) {
        if (isDebug()) {
            logDebug(msg);
        }
    }

    /**
     * Abstract method to log a debug message.
     *
     * @param msg
     */
    protected abstract void logDebug(String msg);
}