/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2005-2007 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id: JResourceLoginModule.java 21566 2011-08-08 12:28:12Z cazauxj $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.security.auth.spi;

import java.util.Map;

import javax.naming.InitialContext;
import javax.naming.NameNotFoundException;
import javax.rmi.PortableRemoteObject;
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.login.LoginException;
import javax.security.auth.spi.LoginModule;

import org.ow2.jonas.lib.security.auth.JSubject;
import org.ow2.jonas.security.auth.callback.CertificateCallback;
import org.ow2.jonas.security.internal.JonasSecurityServiceImpl;
import org.ow2.jonas.security.internal.realm.factory.JResourceRemote;


/**
 * Define a login module for the authentication by using one of the JOnAS
 * resource Datasource, LDAP or file.
 * This LoginModule delegates to the Server the authentication.
 * @author Florent Benoit
 * @author S. Ali Tokmen
 */
public class JResourceLoginModule implements LoginModule {

    /**
     * Default JOnAS server name.
     */
    private static final String DEFAULT_SERVER_NAME = "jonas";

    /**
     * Subject used.
     */
    private Subject subject = null;

    /**
     * Remote subject returned for authentication.
     */
    private Subject remoteSubject = null;

    /**
     * The callbackhandler user for identification.
     */
    private CallbackHandler callbackHandler = null;

    /**
     * Options for this login module.
     */
    private Map options = null;

    /**
     * Password of the principal.
     */
    private String password = null;

    /**
     * Indicates if the login was successfull or not.
     */
    private boolean loginWasDoneWithSuccess = false;

    /**
     * Convert the username into uppercase for the authentication.
     */
    private boolean useUpperCaseUsername = false;

    /**
     * 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(final Subject subject,
                           final CallbackHandler callbackHandler,
                           final Map sharedState,
                           final Map options) {
        this.subject = subject;
        this.callbackHandler = callbackHandler;
        this.options = options;
    }

    /**
     * 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.
     * @return true if the authentication succeeded, or false if this
     *         LoginModule should be ignored.
     * @throws LoginException if the authentication fails
     */
    public boolean login() throws LoginException {

        // No handler
        if (callbackHandler == null) {
            throw new LoginException("No handler has been defined.");
        }

        // Resource to be used (jndi name)
        String resourceName = (String) options.get("resourceName");

        // Name of the server for retrieving the security service
        String serverName = (String) options.get("serverName");
        if (serverName == null) {
            serverName = DEFAULT_SERVER_NAME;
        }

        // use uppercase for authentication
        String useUpperCaseUsernameTxt = (String) options.get("useUpperCaseUsername");
        if ((useUpperCaseUsernameTxt != null) && (Boolean.valueOf(useUpperCaseUsernameTxt).booleanValue())) {
            useUpperCaseUsername = true;
        }

        // Use certificate callback
        String certCallback = (String) options.get("certCallback");

        // No resource is specified -> fail
        if (resourceName == null) {
            throw new LoginException(
                    "You have to give an argument to this login module. The 'resourceName' parameter is required.");
        }

        // Add the security name to servername to find the JNDI object
        String remoteResourceName = serverName + JonasSecurityServiceImpl.REMOTE_RESOUCE;


        // Get the resource and perform authentication
        try {
            InitialContext ictx = new InitialContext();
            JResourceRemote jResourceRemote = null;
            try {
                Object o;
                try {
                    o = ictx.lookup(remoteResourceName);
                } catch(NameNotFoundException e) {
                    // Are we too early? Wait two second and retry
                    Thread.sleep(2000);
                    o = ictx.lookup(remoteResourceName);
                }
                jResourceRemote = (JResourceRemote) PortableRemoteObject.narrow(o, JResourceRemote.class);
            } catch (Exception e) {
                throw createChainedLoginException("Cannot retrieve the resource '" + remoteResourceName
                        + "'. Check that this resource is bound in the registry and that the server name is correct", e);
            }

            // Handle callbacks
            NameCallback nameCallback = new NameCallback("User :");
            // False to hide password when it is entered
            PasswordCallback passwordCallback = new PasswordCallback("Password :", false);
            CertificateCallback certificateCallback = new CertificateCallback();
            Callback[] callbacks = null;

            if ((certCallback != null) && (Boolean.valueOf(certCallback).booleanValue())) {
                callbacks = new Callback[] {nameCallback, passwordCallback, certificateCallback};
            } else {
                callbacks = new Callback[] {nameCallback, passwordCallback};
            }
            callbackHandler.handle(callbacks);

            // Get values
            String principalName = nameCallback.getName();
            if (principalName == null) {
                throw new LoginException("A null username is not a valid username");
            }
            if (principalName.startsWith("##DN##") && (certificateCallback.getUserCertificate() == null)) {
                throw new LoginException("Name must have a certificate to access this certificate based access login");
            }
            char[] arrayPass = passwordCallback.getPassword();
            if (arrayPass == null) {
                throw new LoginException("A null password is not a valid password");
            }

            // Convert into uppercase ?
            if (useUpperCaseUsername) {
                principalName = principalName.toUpperCase();
            }

            password = new String(arrayPass);
            JSubject jSubject = null;
            try {
                jSubject = jResourceRemote.authenticate(principalName, arrayPass, resourceName);
            } catch (Exception e) {
                throw createChainedLoginException("Cannot authenticate with principal name = '"
                                                  + principalName + "' : " + e.getMessage(), e);
            }
            remoteSubject = new Subject();
            remoteSubject.getPrivateCredentials().add(password);
            remoteSubject.getPrincipals().add(jSubject.getName());
            remoteSubject.getPrincipals().add(jSubject.getGroup());

        } catch (Exception e) {
            throw createChainedLoginException("Error during the login phase : " + e.getMessage(), e);
        }
        loginWasDoneWithSuccess = true;
        return true;
    }

    /**
     * Create a LoginException with the given message and set the cause to the given Exception.
     * @param msg Exception message
     * @param e Root cause
     * @return LoginException the chained LoginException
     */
    private static LoginException createChainedLoginException(final String msg, final Exception e) {
        LoginException le = new LoginException(msg);
        le.initCause(e);
        return le;
    }

    /**
     * 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.
     * @return true if this method succeeded, or false if this LoginModule
     *         should be ignored.
     * @throws LoginException if the commit fails
     */
    public boolean commit() throws LoginException {

            //overall authentication succeeded
        if (loginWasDoneWithSuccess && remoteSubject != null) {
            // Add principal to the current subject
            subject.getPrincipals().addAll(remoteSubject.getPrincipals());
            subject.getPrivateCredentials().add(password);
        }
        return loginWasDoneWithSuccess;
    }

    /**
     * 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.
     * @return true if this method succeeded, or false if this LoginModule
     *         should be ignored.
     * @throws LoginException if the abort fails
     */
    public boolean abort() throws LoginException {
        //overall authentication succeeded
        if (loginWasDoneWithSuccess && remoteSubject != null) {
            // Reset temp values
            remoteSubject = null;
        }
        return loginWasDoneWithSuccess;
    }

    /**
     * Method which logs out a Subject. An implementation of this method might
     * remove/destroy a Subject's Principals and Credentials.
     * @return true if this method succeeded, or false if this LoginModule
     *         should be ignored.
     * @throws LoginException if the logout fails
     */
    public boolean logout() throws LoginException {

        //overall authentication succeeded
        if (loginWasDoneWithSuccess && remoteSubject != null) {
            // Remove principal name
            subject.getPrincipals().remove(remoteSubject.getPrincipals());
        }

        return loginWasDoneWithSuccess;
    }

}
