/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2004 Bull S.A.
 * 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
 *
 * Initial developer: Alexandre Thaveau & Marc-Antoine Bourgeot
 * --------------------------------------------------------------------------
 * $Id: CRLLoginModule.java 21566 2011-08-08 12:28:12Z cazauxj $
 * --------------------------------------------------------------------------
 */

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

import java.io.File;
import java.io.FileInputStream;
import java.security.cert.CertStore;
import java.security.cert.CertStoreParameters;
import java.security.cert.CertificateFactory;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.LDAPCertStoreParameters;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLSelector;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Iterator;
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.login.LoginException;
import javax.security.auth.spi.LoginModule;

import org.ow2.jonas.lib.security.auth.JPrincipal;
import org.ow2.jonas.security.auth.callback.CertificateCallback;


/**
 * Defines a login module for the authentication by using certificates
 * @author Alexandre Thaveau (initial developer)
 * @author Marc-Antoine Bourgeot (initial developer)
 */
public class CRLLoginModule implements LoginModule {

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

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

    /**
     * Shared state with all login modules
     */
    private Map sharedState = null;

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

    /**
     * Name of the principal
     */
    private String principalName = null;

    /**
     * Password f the principal
     */
    private String password = null;

    /**
     * Roles of the principal
     */
    private ArrayList principalRoles = null;

    /**
     * Certificate of the user
     */
    private X509Certificate cert = null;

    /**
     * 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.sharedState = sharedState;
        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.");
        }
        String crlsResourceName = (String) options.get("CRLsResourceName");
        String certStoreAccessName = null;
        NameCallback nameCallback = null;
        CertificateCallback certificateCallback = null;
        try {
            // Handle callbacks, here just the certificate and name, name and
            // password are already checked
            nameCallback = new NameCallback("User :");
            certificateCallback = new CertificateCallback();
            Callback[] callbacks = new Callback[] {nameCallback, certificateCallback};
            callbackHandler.handle(callbacks);
        } catch (Exception e) {
            throw new LoginException("Problem while getting informations in the callbackhandler: " + e.getMessage());
        }

        try {
            this.cert = (X509Certificate) certificateCallback.getUserCertificate();
            if (nameCallback.getName().startsWith("##DN##")) {
                if ((this.cert == null)) {
                    throw new LoginException("Client certificate not present, it can be verified with CRL");
                }
            } else {
                //if client to be authenticated doesn't require a certificate
                // based access -> ok
                return true;
            }

            //store the parameters of the CertStore
            CertStoreParameters certStoreParameters = null;
            //two choices : directory, LDAP
            //initialization of an Directory certstore
            if (crlsResourceName.equalsIgnoreCase("Directory")) {
                certStoreAccessName = "Collection";
                // Directory where are stocked the CRLs
                String crlsDirectoryName = (String) options.get("CRLsDirectoryName");
                // No resource is specified -> fail
                if (crlsDirectoryName == null) {
                    throw new LoginException(
                            "You have to give an argument to this login module. The \"CRLsDirectoryName\" parameter is required.");
                }
                File crlsDirectory = new File(crlsDirectoryName);
                if (!crlsDirectory.isDirectory()) {
                    throw new LoginException(crlsDirectoryName + " is not a directory");
                }
                //list files located in the directory
                CertificateFactory cf = CertificateFactory.getInstance("X.509");
                FileInputStream fis = null;
                //file name array for all CRLs
                String[] crlFileName = crlsDirectory.list();
                //collection to stock X509CRL objects
                ArrayList crls = new ArrayList(crlFileName.length);
                X509CRL crl = null;

                for (int i = 0; i < crlFileName.length; i++) {
                    //extension of the file must be .crl
                    if (crlFileName[i].matches(".+\\.crl")) {
                        fis = new FileInputStream(crlsDirectory.getAbsolutePath() + File.separatorChar + crlFileName[i]);
                        crl = (X509CRL) cf.generateCRL(fis);
                        crls.add(crl);
                        fis.close();
                        fis = null;
                        crl = null;
                    }
                }
                //initialize the certstore
                certStoreParameters = new CollectionCertStoreParameters(crls);
            } else if (crlsResourceName.equalsIgnoreCase("LDAP")) {
                //initialization of an LDAP certstore
                certStoreAccessName = "LDAP";
                //get the address and the port of the server
                String address = (String) options.get("address");
                int port = Integer.parseInt((String) options.get("port"));
                if (address == null) {
                    throw new LoginException(
                            "You have to give an argument to this login module. The \"address\" and \"port\" parameter are required.");
                }
                //initialize the certstore
                certStoreParameters = new LDAPCertStoreParameters(address, port);
            } else {
                throw new LoginException(
                        "You have to give an argument to this login module. The \"CRLsResourceName\" is not valid. Must be set to \"Directory\" or \"LDAP\"");
            }

            //check if the client certificate has not been revoked
            CertStore crlsStore = CertStore.getInstance(certStoreAccessName, certStoreParameters);
            X509CRLSelector x509CRLSelector = new X509CRLSelector();
            //add a selection criterion to check the right CRLs, here issuer
            // name
            //of CRL and certificate must be the same
            x509CRLSelector.addIssuerName(this.cert.getIssuerX500Principal().getEncoded());
            Iterator crlIterator = crlsStore.getCRLs(x509CRLSelector).iterator();
            while (crlIterator.hasNext()) {
                if (((X509CRL) crlIterator.next()).isRevoked(this.cert)) {
                    throw new LoginException("Client certificate has been revoked");
                }
            }
        } catch (Exception e) {
            throw new LoginException("Error during the login phase : " + e.getMessage());
        }

        return true;
    }

    /**
     * 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 {
        return true;
    }

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

        // Reset temp values
        principalName = null;
        principalRoles = null;

        return true;
    }

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

        // Remove principal name
        subject.getPrincipals().remove(new JPrincipal(principalName));

        return true;
    }

}