Package org.jboss.soa.esb.services.security.auth.login

Source Code of org.jboss.soa.esb.services.security.auth.login.CertificateLoginModule

/*
* JBoss, Home of Professional Open Source Copyright 2008, Red Hat Middleware
* LLC, and individual contributors by the @authors tag. See the copyright.txt
* in the distribution for a full listing of individual contributors.
*
* This 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 (at your option)
* any later version.
*
* This software 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 software; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF
* site: http://www.fsf.org.
*/
package org.jboss.soa.esb.services.security.auth.login;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Principal;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

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.LoginException;
import javax.security.auth.spi.LoginModule;

import org.apache.log4j.Logger;
import org.jboss.security.auth.callback.ObjectCallback;
import org.jboss.soa.esb.services.security.principals.Group;
import org.jboss.soa.esb.services.security.principals.Role;
import org.jboss.soa.esb.services.security.principals.User;
import org.jboss.soa.esb.util.ClassUtil;

/**
* A JAAS Login module that performs authentication by verifying that the
* certificate that is passed to the ESB by the calling client can be verified
* against a certificate in a local keystore.
* <p/>
*
* Usage:
* <pre>
* CertLogin {
*  org.jboss.soa.esb.services.security.auth.login.CertificateLoginModule required keyStoreURL="file://keystore" keyStorePassword="jbossesb" rolesPropertiesFile="file://roles.properties";
* };
* </pre>
*
* Option description:
* <lu>
<li>keyStoreURL - URL or simply a path to a file on the local file system or on the classpath</li>
<li>keyStorePassword - password for the above keystore</li>
<li>rolesPropertiesFile - URL or simply a path to a file on the local file sytem of on the classpath that contains user to role mappings:
*  user=role1,role2
</li>
* </lu>
*
* @author <a href="mailto:dbevenius@jboss.com">Daniel Bevenius</a>
*
*/
public class CertificateLoginModule implements LoginModule
{
    public static final String KEYSTORE_URL = "keyStoreURL";
    public static final String KEYSTORE_PASSWORD = "keyStorePassword";
    public static final String KEYSTORE_TYPE = "keyStoreType";
    public static final String ROLE_PROPERTIES = "rolesPropertiesFile";

    private Logger log = Logger.getLogger(CertificateLoginModule.class);

    private Subject subject;
    private CallbackHandler callbackHandler;
    private Map<String, ?> options;
    private X509Certificate verifiedCertificate;

    /**
     * Initialized this login module. Simple stores the passed in fields and also validates the options.
     *
     * @param subject The subject to authenticate/populate.
     * @param callbackHandler The callbackhandler that will gather information required by this login module.
     * @param sharedState State that is shared with other login modules. Used when modules are chained/stacked.
     * @param options The options that were specified for this login module. See "Usage" section of this types javadoc.
     */
    public void initialize(final Subject subject, final CallbackHandler callbackHandler, final Map<String, ?> sharedState, final Map<String, ?> options)
    {
        this.subject = subject;
        this.callbackHandler = callbackHandler;
        this.options = options;
    }

    /**
     * Login performs the verification of the callers certificate against the alias
     * that that is provided by the callback handler.
     *
     * @return true If the login was successful otherwise false.
     * @throws LoginException If an error occurs while trying to perform the authentication.
     */
    public boolean login() throws LoginException
    {
        assertOptions(options);
        assertCallbackHandler(callbackHandler);

        final NameCallback aliasCallback = new NameCallback("Key Alias: ");
        final PasswordCallback passwordCallback = new PasswordCallback("Key Password", false);
        final ObjectCallback objectCallback = new ObjectCallback("Certificate: ");

        try
        {
            // get information from caller
            callbackHandler.handle(new Callback[]{aliasCallback, passwordCallback, objectCallback});
        }
        catch (final IOException e)
        {
            throw new LoginException("Failed to invoke callback: "+ e.toString());
        }
        catch (final UnsupportedCallbackException e)
        {
            throw new LoginException("CallbackHandler does not support: " + e.getCallback());
        }

        final X509Certificate callerCert = getCallerCertificate(objectCallback);
        final String alias = getAlias(aliasCallback);
        final KeyStore keyStore = loadKeyStore();
        try
        {
            //  get the certificate that matches the alias from the keystore
            final Certificate esbCertificate = keyStore.getCertificate(alias);
            if (esbCertificate == null)
            {
                throw new LoginException("No certificate found in keystore for alias '" + alias + "'");
            }
            //  verify that the caller supplied certificate was signed using the public key in our keystore.
            callerCert.verify(esbCertificate.getPublicKey());

            //  set the verified certificate. Will be used in commit to add principals to the subject.
            this.verifiedCertificate = callerCert;
            return true;
        }
        catch (final KeyStoreException e)
        {
            throw new LoginException("KeystoreException : " + e.getMessage());
        }
        catch (final NoSuchAlgorithmException e)
        {
            throw new LoginException("NoSuchAlgorithmException : " + e.getMessage());
        }
        catch (final InvalidKeyException e)
        {
            throw new LoginException("InvalidKeyExcpetion : " + e.getMessage());
        }
        catch (final NoSuchProviderException e)
        {
            throw new LoginException("NoSuchProviderException : " + e.getMessage());
        }
        catch (final SignatureException e)
        {
            throw new LoginException("SignatureException : " + e.getMessage());
        }
        catch (final CertificateException e)
        {
            throw new LoginException("CertificateException : " + e.getMessage());
        }
    }

    /**
     * If the login was successful this method adds principals and roles to the subject.
     * When adding a Principal we simply use the Common Name(CN) from the Distinguished Name(DN).
     *
     */
    public boolean commit() throws LoginException
    {
        if (verifiedCertificate == null)
        {
           return false;
        }
        else
        {
            final Set<Principal> principals = subject.getPrincipals();
            String name = verifiedCertificate.getSubjectX500Principal().getName();
            // get the CN from the DN.
            name = name.substring(name.indexOf('=') + 1, name.indexOf(','));
            final User authenticatedPrincipal = new User(name);
            principals.add(authenticatedPrincipal);

            addRoles(subject, authenticatedPrincipal, verifiedCertificate, Collections.unmodifiableMap(options));
            return true;
        }
    }

    public boolean abort() throws LoginException
    {
        return false;
    }

    public boolean logout() throws LoginException
    {
        verifiedCertificate = null;
        return false;
    }

    /**
     * The addRoles method add roles to the authenticated subject.
     * This method is protected to let users easliy override only this method if they
     * need a different behaviour.
     *
     * @param subject The subject
     * @param principal The authenticated principal
     * @param cert  The certificate that of the authenticated principal
     * @param options The options that were specified to this login module.
     * @throws LoginException
     */
    protected void addRoles(final Subject subject, final Principal principal, final X509Certificate cert, final Map<String, ?> options) throws LoginException
    {
        final String roleProperties = (String) options.get(ROLE_PROPERTIES);
        if (roleProperties == null)
        {
            log.warn("No " + ROLE_PROPERTIES + " was specified hence no roles will be added.");
        }
        else
        {
            InputStream resourceAsStream = getResourceAsStream(roleProperties, getClass());
            if (resourceAsStream == null )
            {
                throw new LoginException(ROLE_PROPERTIES + " was specified as '" + roleProperties + "' but could not be located on the local file system or on the classpath. Please check the configuration.");
            }
            try
            {
                final Properties roles = new Properties();
                //  load the roles properties file
                roles.load(resourceAsStream);

                //  get the list of roles specified for the authenticated principal
                final String listOfRoles = (String)roles.get(principal.getName());
                if (listOfRoles != null )
                {
                    log.debug("Roles for " + principal.getName() + " [" + listOfRoles + "]");
                    for (String role : listOfRoles.split(","))
                    {
                        addRole(role, subject);
                    }
                }
            }
            catch (final IOException e)
            {
                throw new LoginException("IOException while trying to read properties from '" + roleProperties + "'");
            }
            finally
            {
                try { resourceAsStream.close(); } catch (final IOException ignore) { log.error(ignore.getMessage(), ignore);}
            }
        }
    }

    private void addRole(final String roleName, final Subject subject )
    {
        if (roleName != null)
        {
            final Role role = new Role(roleName);
            final Set<Group> principals = subject.getPrincipals(Group.class);
            if ( principals.isEmpty() )
            {
                final Group group = new Group("Roles");
                group.addMember(role);
                subject.getPrincipals().add(group);
            }
            else
            {
                for (Group groups : principals)
                {
                    if ( "Roles".equals(groups.getName()) )
                    {
                        groups.addMember(role);
                    }
                }
            }
        }
    }

    /**
     * Assert that the required options have been specified for this login module.
     * Mandatory options are:
     * <lu>
     <li>keyStoreURL</li>
     <li>keyStorePassword</li>
     * </lu>
     * @param options The options that were specified.
     * @throws LoginException If a mandatory option was missing.
     */
    void assertOptions(final Map<String, ?> options) throws LoginException
    {
        if (options == null || options.isEmpty() || !options.containsKey(KEYSTORE_URL) || !options.containsKey(KEYSTORE_PASSWORD))
        {
            throw new LoginException(getMissingRequiredOptionString(options));
        }
    }

    private KeyStore loadKeyStore() throws LoginException
    {
        final String keyStorePath = (String)options.get(KEYSTORE_URL);
        KeyStore keystore = null;
        InputStream in = null;
        try
        {
            String keyStoreType = (String)options.get(KEYSTORE_TYPE);
            if (keyStoreType == null)
            {
                keyStoreType = KeyStore.getDefaultType();
            }

            keystore = KeyStore.getInstance(keyStoreType);
            in = getResourceAsStream(keyStorePath, getClass());
            if (in == null)
            {
                throw new LoginException("Could not open a stream to the keystore '" + keyStorePath + "'");
            }
            keystore.load(in, ((String)options.get(KEYSTORE_PASSWORD)).toCharArray());

            log.info("Successfully loaded keystore: '" + keyStorePath + "'");
        }
        catch (final KeyStoreException e)
        {
            throw new LoginException("KeyStoreException while trying to load keystore '" + keyStorePath + "': " + e.getMessage());
        }
        catch (NoSuchAlgorithmException e)
        {
            throw new LoginException("NoSuchAlgorithm while trying to load keystore '" + keyStorePath + "': " + e.getMessage());
        }
        catch (CertificateException e)
        {
            throw new LoginException("CertificateException while trying to load keystore '" + keyStorePath + "': " + e.getMessage());
        }
        catch (IOException e)
        {
            throw new LoginException("IOException while trying to load keystore '" + keyStorePath + "': " + e.getMessage());
        }
        finally
        {
            if (in != null) { try { in.close(); } catch (final IOException e) { log.error("Error while closing stream to keystore '" + keyStorePath + "'", e); } }
        }
        return keystore;
    }

    /**
     * Get an string contain the options that were missing in the configuration
     * for this login module.
     *
     * @param options The map of options that were specified for this login module.
     * @return String A string that contains only the options that were not specified.
     */
    private String getMissingRequiredOptionString(final Map<String, ?> options)
    {

        final StringBuilder sb = new StringBuilder();
        sb.append("Options missing [");

        if (options == null || !options.containsKey(KEYSTORE_URL))
        {
            sb.append(KEYSTORE_URL).append(", ");
        }

        if (options == null || !options.containsKey(KEYSTORE_PASSWORD))
        {
            sb.append(KEYSTORE_PASSWORD).append(",");
        }

        sb.append("]");

        return sb.toString();
    }

    private void assertCallbackHandler(final CallbackHandler handler) throws LoginException
    {
        if (callbackHandler == null)
        {
            throw new LoginException("No callback handler was specified for CertificateLoginModule.");
        }
    }

    private X509Certificate getCallerCertificate(final ObjectCallback objectCallback) throws LoginException
    {
        final Set<?> credentials = (Set<?>) objectCallback.getCredential();
        if (credentials == null || credentials.isEmpty())
        {
            throw new LoginException("No X509Certificate was passed to the login module");
        }

        X509Certificate callerCert = null;
        for (Object object : credentials)
        {
            if (object instanceof X509Certificate)
            {
                callerCert = (X509Certificate) object;
                break;
            }
        }

        if (callerCert == null)
        {
            throw new LoginException("No X509Certificate was passed to the login module");
        }

        return callerCert;
    }

    private String getAlias(final NameCallback callback) throws LoginException
    {
        final String alias = callback.getName();
        if (alias == null)
        {
            throw new LoginException("No X509Certificate was passed to the login module");
        }
        else
        {
            return callback.getName();
        }
    }

    /**
     * Get the specified resource as a stream. First try the resource as a file
     * from the file system, and if not found try the classpath.
     * <p/>
     * The method performs the file system search but delegates the classpath
     * lookup to {@link ClassUtil}.
     *
     * @param resourceName The name of the class to load.
     * @param caller The class of the caller.
     * @return The input stream for the resource or null if not found.
     */
    private InputStream getResourceAsStream(final String resourceName, final Class<?> caller)
    {
        URL fileUrl = null;
        File file = null;
        try
        {
            // try to parse the resouceName as an url.
            fileUrl = new URL(resourceName);
            file = new File(fileUrl.getFile());
        }
        catch (MalformedURLException ignored)
        {
            file = new File(resourceName);
        }

        if (file.exists() && file.isFile())
        {
            try
            {
                return new FileInputStream(file);
            }
            catch (final FileNotFoundException ignore)
            {
                // will revert to looking for the resource using the classpath
            }
        }
        return ClassUtil.getResourceAsStream(resourceName, caller);
    }

}
TOP

Related Classes of org.jboss.soa.esb.services.security.auth.login.CertificateLoginModule

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.