/* * JBoss, Home of Professional Open Source * Copyright 2005, JBoss Inc., and individual contributors as indicated * 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.security.auth.spi; import java.io.IOException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.Principal; import java.security.acl.Group; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Enumeration; import java.util.Map; import javax.naming.InitialContext; import javax.naming.NamingException; 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.UnsupportedCallbackException; import javax.security.auth.login.LoginException; import org.jboss.security.JSSESecurityDomain; import org.jboss.security.PicketBoxLogger; import org.jboss.security.PicketBoxMessages; import org.jboss.security.SecurityConstants; import org.jboss.security.SecurityDomain; import org.jboss.security.SecurityUtil; import org.jboss.security.auth.callback.ObjectCallback; import org.jboss.security.auth.certs.X509CertificateVerifier; /** * Base Login Module that uses X509Certificates as credentials for * authentication. * * This login module uses X509Certificates as a * credential. It takes the cert as an object and checks to see if the alias in * the truststore/keystore contains the same certificate. Subclasses of this * module should implement the getRoleSets() method defined by * AbstractServerLoginModule. Much of this module was patterned after the * UserNamePasswordLoginModule. * * @author <a href="mailto:jasone@greenrivercomputing.com">Jason Essington</a> * @author Scott.Stark@jboss.org * @version $Revision$ */ public class BaseCertLoginModule extends AbstractServerLoginModule { // see AbstractServerLoginModule private static final String SECURITY_DOMAIN = "securityDomain"; private static final String VERIFIER = "verifier"; private static final String[] ALL_VALID_OPTIONS = { SECURITY_DOMAIN,VERIFIER }; /** A principal derived from the certificate alias */ private Principal identity; /** The client certificate */ private X509Certificate credential; /** The SecurityDomain to obtain the KeyStore/TrustStore from */ private Object domain = null; /** An option certificate verifier */ private X509CertificateVerifier verifier; /** Override the super version to pickup the following options after first * calling the super method. * * option: securityDomain - the name of the SecurityDomain to obtain the * trust and keystore from. * option: verifier - the class name of the X509CertificateVerifier to use * for verification of the login certificate * * @see SecurityDomain * @see X509CertificateVerifier * * @param subject the Subject to update after a successful login. * @param callbackHandler the CallbackHandler that will be used to obtain the * the user identity and credentials. * @param sharedState a Map shared between all configured login module instances * @param options the parameters passed to the login module. */ public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String,?> sharedState, Map<String,?> options) { addValidOptions(ALL_VALID_OPTIONS); super.initialize(subject, callbackHandler, sharedState, options); // Get the security domain and default to "other" String sd = (String) options.get(SECURITY_DOMAIN); sd = SecurityUtil.unprefixSecurityDomain(sd); if (sd == null) sd = "other"; try { Object tempDomain = new InitialContext().lookup(SecurityConstants.JAAS_CONTEXT_ROOT + sd); if (tempDomain instanceof SecurityDomain) { domain = tempDomain; if (PicketBoxLogger.LOGGER.isTraceEnabled()) { PicketBoxLogger.LOGGER.traceSecurityDomainFound(domain.getClass().getName()); } } else { tempDomain = new InitialContext().lookup(SecurityConstants.JAAS_CONTEXT_ROOT + sd + "/jsse"); if (tempDomain instanceof JSSESecurityDomain) { domain = tempDomain; if (PicketBoxLogger.LOGGER.isTraceEnabled()) { PicketBoxLogger.LOGGER.traceSecurityDomainFound(domain.getClass().getName()); } } else { PicketBoxLogger.LOGGER.errorGettingJSSESecurityDomain(sd); } } } catch (NamingException e) { PicketBoxLogger.LOGGER.errorFindingSecurityDomain(sd, e); } String option = (String) options.get(VERIFIER); if( option != null ) { try { ClassLoader loader = SecurityActions.getContextClassLoader(); Class<?> verifierClass = loader.loadClass(option); verifier = (X509CertificateVerifier) verifierClass.newInstance(); } catch(Throwable e) { PicketBoxLogger.LOGGER.errorCreatingCertificateVerifier(e); } } PicketBoxLogger.LOGGER.traceEndInitialize(); } /** * Perform the authentication of the username and password. */ @SuppressWarnings("unchecked") public boolean login() throws LoginException { PicketBoxLogger.LOGGER.traceBeginLogin(); // See if shared credentials exist if (super.login() == true) { // Setup our view of the user Object username = sharedState.get("javax.security.auth.login.name"); if( username instanceof Principal ) identity = (Principal) username; else { String name = username.toString(); try { identity = createIdentity(name); } catch(Exception e) { throw PicketBoxMessages.MESSAGES.failedToCreatePrincipal(e.getLocalizedMessage()); } } Object password = sharedState.get("javax.security.auth.login.password"); if (password instanceof X509Certificate) credential = (X509Certificate) password; else if (password != null) { PicketBoxLogger.LOGGER.debugPasswordNotACertificate(); super.loginOk = false; return false; } return true; } super.loginOk = false; Object[] info = getAliasAndCert(); String alias = (String) info[0]; credential = (X509Certificate) info[1]; if (alias == null && credential == null) { identity = unauthenticatedIdentity; if (PicketBoxLogger.LOGGER.isTraceEnabled()) { PicketBoxLogger.LOGGER.traceUsingUnauthIdentity(identity.toString()); } } if (identity == null) { try { identity = createIdentity(alias); } catch(Exception e) { PicketBoxLogger.LOGGER.debugFailureToCreateIdentityForAlias(alias, e); } if (!validateCredential(alias, credential)) { throw PicketBoxMessages.MESSAGES.failedToMatchCredential(alias); } } if (getUseFirstPass() == true) { // Add authentication info to shared state map sharedState.put("javax.security.auth.login.name", alias); sharedState.put("javax.security.auth.login.password", credential); } super.loginOk = true; PicketBoxLogger.LOGGER.traceEndLogin(super.loginOk); return true; } /** Override to add the X509Certificate to the public credentials * @return * @throws LoginException */ public boolean commit() throws LoginException { boolean ok = super.commit(); if( ok == true ) { // Add the cert to the public credentials if (credential != null) { subject.getPublicCredentials().add(credential); } } return ok; } /** Subclasses need to override this to provide the roles for authorization * @return * @throws LoginException */ protected Group[] getRoleSets() throws LoginException { return new Group[0]; } protected Principal getIdentity() { return identity; } protected Object getCredentials() { return credential; } protected String getUsername() { String username = null; if (getIdentity() != null) username = getIdentity().getName(); return username; } protected Object[] getAliasAndCert() throws LoginException { PicketBoxLogger.LOGGER.traceBeginGetAliasAndCert(); Object[] info = { null, null }; // prompt for a username and password if (callbackHandler == null) { throw PicketBoxMessages.MESSAGES.noCallbackHandlerAvailable(); } NameCallback nc = new NameCallback("Alias: "); ObjectCallback oc = new ObjectCallback("Certificate: "); Callback[] callbacks = { nc, oc }; String alias = null; X509Certificate cert = null; X509Certificate[] certChain; try { callbackHandler.handle(callbacks); alias = nc.getName(); Object tmpCert = oc.getCredential(); if (tmpCert != null) { if (tmpCert instanceof X509Certificate) { cert = (X509Certificate) tmpCert; if (PicketBoxLogger.LOGGER.isTraceEnabled()) { PicketBoxLogger.LOGGER.traceCertificateFound(cert.getSerialNumber().toString(16), cert.getSubjectDN().getName()); } } else if( tmpCert instanceof X509Certificate[] ) { certChain = (X509Certificate[]) tmpCert; if( certChain.length > 0 ) cert = certChain[0]; } else { throw PicketBoxMessages.MESSAGES.unableToGetCertificateFromClass(tmpCert != null ? tmpCert.getClass() : null); } } else { PicketBoxLogger.LOGGER.warnNullCredentialFromCallbackHandler(); } } catch (IOException e) { LoginException le = PicketBoxMessages.MESSAGES.failedToInvokeCallbackHandler(); le.initCause(e); throw le; } catch (UnsupportedCallbackException uce) { LoginException le = new LoginException(); le.initCause(uce); throw le; } info[0] = alias; info[1] = cert; PicketBoxLogger.LOGGER.traceEndGetAliasAndCert(); return info; } protected boolean validateCredential(String alias, X509Certificate cert) { PicketBoxLogger.LOGGER.traceBeginValidateCredential(); boolean isValid = false; // if we don't have a trust store, we'll just use the key store. KeyStore keyStore = null; KeyStore trustStore = null; if( domain != null ) { if (domain instanceof SecurityDomain) { keyStore = ((SecurityDomain) domain).getKeyStore(); trustStore = ((SecurityDomain) domain).getTrustStore(); } else if (domain instanceof JSSESecurityDomain) { keyStore = ((JSSESecurityDomain) domain).getKeyStore(); trustStore = ((JSSESecurityDomain) domain).getTrustStore(); } } if( trustStore == null ) trustStore = keyStore; if( verifier != null ) { // Have the verifier validate the cert PicketBoxLogger.LOGGER.traceValidatingUsingVerifier(verifier.getClass()); isValid = verifier.verify(cert, alias, keyStore, trustStore); } else if (trustStore != null && cert != null) { // Look for the cert in the truststore using the alias X509Certificate storeCert = null; try { storeCert = (X509Certificate) trustStore.getCertificate(alias); if(PicketBoxLogger.LOGGER.isTraceEnabled()) { StringBuffer buf = new StringBuffer("\n\t"); buf.append(PicketBoxMessages.MESSAGES.suppliedCredentialMessage()); buf.append(cert.getSerialNumber().toString(16)); buf.append("\n\t\t"); buf.append(cert.getSubjectDN().getName()); buf.append("\n\n\t"); buf.append(PicketBoxMessages.MESSAGES.existingCredentialMessage()); if( storeCert != null ) { buf.append(storeCert.getSerialNumber().toString(16)); buf.append("\n\t\t"); buf.append(storeCert.getSubjectDN().getName()); buf.append("\n"); } else { ArrayList<String> aliases = new ArrayList<String>(); Enumeration<String> en = trustStore.aliases(); while (en.hasMoreElements()) { aliases.add(en.nextElement()); } buf.append(PicketBoxMessages.MESSAGES.noMatchForAliasMessage(alias, aliases)); } PicketBoxLogger.LOGGER.trace(buf.toString()); } } catch (KeyStoreException e) { PicketBoxLogger.LOGGER.warnFailureToFindCertForAlias(alias, e); } // Ensure that the two certs are equal if (cert.equals(storeCert)) isValid = true; } else { PicketBoxLogger.LOGGER.warnFailureToValidateCertificate(); } PicketBoxLogger.LOGGER.traceEndValidateCredential(isValid); return isValid; } }