/* * JBoss, Home of Professional Open Source. * Copyright 2011, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. See the copyright.txt file 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.authentication; import java.io.Serializable; import java.lang.reflect.Method; import java.security.Principal; import java.security.acl.Group; import java.util.Arrays; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentMap; import javax.security.auth.Subject; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import org.jboss.security.AuthenticationManager; import org.jboss.security.CacheableManager; import org.jboss.security.PicketBoxLogger; import org.jboss.security.PicketBoxMessages; import org.jboss.security.SecurityConstants; import org.jboss.security.SecurityContext; import org.jboss.security.SecurityContextAssociation; import org.jboss.security.auth.callback.JBossCallbackHandler; import org.jboss.security.auth.login.BaseAuthenticationInfo; import org.jboss.security.authentication.JBossCachedAuthenticationManager.DomainInfo; import org.jboss.security.config.ApplicationPolicy; import org.jboss.security.config.SecurityConfiguration; import org.jboss.security.plugins.ClassLoaderLocator; import org.jboss.security.plugins.ClassLoaderLocatorFactory; /** * {@link AuthenticationManager} implementation that uses {@code CacheableManager} as the cache provider. * * @author <a href="mmoyses@redhat.com">Marcus Moyses</a> * @author <a href="on@ibis.odessa.ua">Oleg Nitz</a> * @author Scott.Stark@jboss.org * @author Anil.Saldhana@jboss.org */ public class JBossCachedAuthenticationManager implements AuthenticationManager, CacheableManager<ConcurrentMap<Principal, DomainInfo>, Principal> { private String securityDomain; private CallbackHandler callbackHandler; private transient Method setSecurityInfo; protected ConcurrentMap<Principal, DomainInfo> domainCache; private boolean deepCopySubjectOption = false; /** * Create a new JBossCachedAuthenticationManager using the * default security domain and {@link CallbackHandler} implementation. */ public JBossCachedAuthenticationManager() { this(SecurityConstants.DEFAULT_APPLICATION_POLICY, new JBossCallbackHandler()); } /** * Create a new JBossCachedAuthenticationManager. * * @param securityDomain name of the security domain * @param callbackHandler {@link CallbackHandler} implementation */ public JBossCachedAuthenticationManager(String securityDomain, CallbackHandler callbackHandler) { this.securityDomain = securityDomain; this.callbackHandler = callbackHandler; // Get the setSecurityInfo(Principal principal, Object credential) method Class<?>[] sig = {Principal.class, Object.class}; try { setSecurityInfo = callbackHandler.getClass().getMethod("setSecurityInfo", sig); } catch (Exception e) { throw new UnsupportedOperationException(PicketBoxMessages.MESSAGES.unableToFindSetSecurityInfoMessage()); } } public Subject getActiveSubject() { Subject subj = null; SecurityContext sc = SecurityContextAssociation.getSecurityContext(); if (sc != null) { subj = sc.getUtil().getSubject(); } return subj; } public Principal getTargetPrincipal(Principal anotherDomainPrincipal, Map<String, Object> contextMap) { throw new UnsupportedOperationException(); } public boolean isValid(Principal principal, Object credential) { return isValid(principal, credential, null); } public boolean isValid(Principal principal, Object credential, Subject activeSubject) { // first check cache DomainInfo cachedEntry = getCacheInfo(principal != null ? principal : new org.jboss.security.SimplePrincipal("null")); if (PicketBoxLogger.LOGGER.isTraceEnabled()) { PicketBoxLogger.LOGGER.traceBeginIsValid(principal, cachedEntry != null ? cachedEntry.toString() : null); } boolean isValid = false; if (cachedEntry != null) { isValid = validateCache(cachedEntry, credential, activeSubject); } if (!isValid) isValid = authenticate(principal, credential, activeSubject); PicketBoxLogger.LOGGER.traceEndIsValid(isValid); return isValid; } public String getSecurityDomain() { return securityDomain; } public void flushCache() { PicketBoxLogger.LOGGER.traceFlushWholeCache(); if (domainCache != null) { for (Principal principal : domainCache.keySet()) { this.flushCache(principal); } } } public void flushCache(Principal key) { if (key != null && this.domainCache != null && this.domainCache.containsKey(key)) // this is currently done to preserve backwards compatibility - logout removes the entry from the cache and performs // the JAAS logout. this.logout(key, null); } public void setCache(ConcurrentMap<Principal, DomainInfo> cache) { this.domainCache = cache; } public boolean containsKey(Principal key) { if (domainCache != null && key != null) return domainCache.containsKey(key); return false; } public Set<Principal> getCachedKeys() { if (domainCache != null) return domainCache.keySet(); return null; } /** * Flag to specify if deep copy of subject sets needs to be * enabled * * @param flag */ public void setDeepCopySubjectOption(Boolean flag) { deepCopySubjectOption = flag.booleanValue(); } /** * Retrieve on entry from the cache. * * @param principal entry's key * @return entry's value or null if not found */ private DomainInfo getCacheInfo(Principal principal) { if (domainCache != null && principal != null) return domainCache.get(principal); return null; } /** * Validate the cache credential value against the provided credential */ @SuppressWarnings({"rawtypes", "unchecked"}) private boolean validateCache(DomainInfo info, Object credential, Subject theSubject) { if (PicketBoxLogger.LOGGER.isTraceEnabled()) { PicketBoxLogger.LOGGER.traceBeginValidateCache(info.toString(), credential != null ? credential.getClass() : null); } Object subjectCredential = info.credential; boolean isValid = false; // Check for a null credential as can be the case for an anonymous user if (credential == null || subjectCredential == null) { // Both credentials must be null isValid = (credential == null) && (subjectCredential == null); } // See if the credential is assignable to the cache value else if (subjectCredential.getClass().isAssignableFrom(credential.getClass())) { // Validate the credential by trying Comparable, char[], byte[], Object[], and finally Object.equals() if (subjectCredential instanceof Comparable) { Comparable c = (Comparable) subjectCredential; isValid = c.compareTo(credential) == 0; } else if (subjectCredential instanceof char[]) { char[] a1 = (char[]) subjectCredential; char[] a2 = (char[]) credential; isValid = Arrays.equals(a1, a2); } else if (subjectCredential instanceof byte[]) { byte[] a1 = (byte[]) subjectCredential; byte[] a2 = (byte[]) credential; isValid = Arrays.equals(a1, a2); } else if (subjectCredential.getClass().isArray()) { Object[] a1 = (Object[]) subjectCredential; Object[] a2 = (Object[]) credential; isValid = Arrays.equals(a1, a2); } else { isValid = subjectCredential.equals(credential); } } else if (subjectCredential instanceof char[] && credential instanceof String) { char[] a1 = (char[]) subjectCredential; char[] a2 = ((String) credential).toCharArray(); isValid = Arrays.equals(a1, a2); } else if (subjectCredential instanceof String && credential instanceof char[]) { char[] a1 = ((String) subjectCredential).toCharArray(); char[] a2 = (char[]) credential; isValid = Arrays.equals(a1, a2); } // If the credentials match, set the thread's active Subject if (isValid) { // Copy the current subject into theSubject if (theSubject != null) { SubjectActions.copySubject(info.subject, theSubject, false, this.deepCopySubjectOption); } } PicketBoxLogger.LOGGER.traceEndValidteCache(isValid); return isValid; } /** * Currently this simply calls defaultLogin() to do a JAAS login using the * security domain name as the login module configuration name. * * @param principal - the user id to authenticate * @param credential - an opaque credential. * @return false on failure, true on success. */ private boolean authenticate(Principal principal, Object credential, Subject theSubject) { ApplicationPolicy theAppPolicy = SecurityConfiguration.getApplicationPolicy(securityDomain); if(theAppPolicy != null) { BaseAuthenticationInfo authInfo = theAppPolicy.getAuthenticationInfo(); List<String> jbossModuleNames = authInfo.getJBossModuleNames(); if(!jbossModuleNames.isEmpty()) { ClassLoader currentTccl = SubjectActions.getContextClassLoader(); ClassLoaderLocator theCLL = ClassLoaderLocatorFactory.get(); if(theCLL != null) { ClassLoader newTCCL = theCLL.get(jbossModuleNames); if(newTCCL != null) { try { SubjectActions.setContextClassLoader(newTCCL); return proceedWithJaasLogin(principal, credential, theSubject, newTCCL); } finally { SubjectActions.setContextClassLoader(currentTccl); } } } } } return proceedWithJaasLogin(principal, credential, theSubject, null); } private boolean proceedWithJaasLogin(Principal principal, Object credential, Subject theSubject, ClassLoader contextClassLoader) { Subject subject = null; boolean authenticated = false; LoginException authException = null; try { // Validate the principal using the login configuration for this domain LoginContext lc = defaultLogin(principal, credential); subject = lc.getSubject(); // Set the current subject if login was successful if (subject != null) { // Copy the current subject into theSubject if (theSubject != null) { SubjectActions.copySubject(subject, theSubject, false, this.deepCopySubjectOption); } else { theSubject = subject; } authenticated = true; // Build the Subject based DomainInfo cache value updateCache(lc, subject, principal, credential, contextClassLoader); } } catch (LoginException e) { PicketBoxLogger.LOGGER.debugFailedLogin(e); authException = e; } // Set the security association thread context info exception SubjectActions.setContextInfo("org.jboss.security.exception", authException); return authenticated; } /** * Pass the security info to the login modules configured for * this security domain using our SecurityAssociationHandler. * * @return The authenticated Subject if successful. * @exception LoginException throw if login fails for any reason. */ private LoginContext defaultLogin(Principal principal, Object credential) throws LoginException { // We use our internal CallbackHandler to provide the security info. A // copy must be made to ensure there is a unique handler per active // login since there can be multiple active logins. Object[] securityInfo = {principal, credential}; CallbackHandler theHandler = null; try { theHandler = (CallbackHandler) callbackHandler.getClass().newInstance(); setSecurityInfo.invoke(theHandler, securityInfo); } catch (Throwable e) { LoginException le = new LoginException(PicketBoxMessages.MESSAGES.unableToFindSetSecurityInfoMessage()); le.initCause(e); throw le; } Subject subject = new Subject(); LoginContext lc = null; PicketBoxLogger.LOGGER.traceDefaultLoginPrincipal(principal); lc = SubjectActions.createLoginContext(securityDomain, subject, theHandler); lc.login(); if (PicketBoxLogger.LOGGER.isTraceEnabled()) { PicketBoxLogger.LOGGER.traceDefaultLoginSubject(lc.toString(), SubjectActions.toString(subject)); } return lc; } /** * Updates the cache either by inserting a new entry or by replacing * an invalid (expired) entry. * * @param loginContext {@link LoginContext} of the authentication * @param subject {@link Subject} resulted from JAAS login * @param principal {@link Principal} representing the user's identity * @param credential user's proof of identity * @return authenticated {@link Subject} */ private Subject updateCache(LoginContext loginContext, Subject subject, Principal principal, Object credential, ClassLoader lcClassLoader) { // If we don't have a cache there is nothing to update if (domainCache == null) return subject; DomainInfo info = new DomainInfo(); info.loginContext = loginContext; info.subject = new Subject(); SubjectActions.copySubject(subject, info.subject, true, this.deepCopySubjectOption); info.credential = credential; if (lcClassLoader == null) { lcClassLoader = java.security.AccessController.doPrivileged(new java.security.PrivilegedAction<ClassLoader>() { public ClassLoader run() { ClassLoader loader = Thread.currentThread().getContextClassLoader(); if (loader == null) { loader = ClassLoader.getSystemClassLoader(); } return loader; } }); } info.contextClassLoader = lcClassLoader; if (PicketBoxLogger.LOGGER.isTraceEnabled()) { PicketBoxLogger.LOGGER.traceUpdateCache(SubjectActions.toString(subject), SubjectActions.toString(info.subject)); } // Get the Subject callerPrincipal by looking for a Group called 'CallerPrincipal' Set<Group> subjectGroups = subject.getPrincipals(Group.class); Iterator<Group> iter = subjectGroups.iterator(); while (iter.hasNext()) { Group grp = iter.next(); String name = grp.getName(); if (name.equals("CallerPrincipal")) { Enumeration<? extends Principal> members = grp.members(); if (members.hasMoreElements()) info.callerPrincipal = members.nextElement(); } } // Handle null principals with no callerPrincipal. This is an indication // of an user that has not provided any authentication info, but // has been authenticated by the domain login module stack. Here we look // for the first non-Group Principal and use that. if (info.callerPrincipal == null) { Set<Principal> subjectPrincipals = subject.getPrincipals(Principal.class); Iterator<? extends Principal> iterPrincipals = subjectPrincipals.iterator(); while (iterPrincipals.hasNext()) { Principal p = iterPrincipals.next(); if (!(p instanceof Group)) { info.callerPrincipal = p; break; } } } // If the user already exists another login is active. Currently // only one is allowed so remove the old and insert the new domainCache.put(principal != null ? principal : new org.jboss.security.SimplePrincipal("null"), info); if (PicketBoxLogger.LOGGER.isTraceEnabled()) { PicketBoxLogger.LOGGER.traceInsertedCacheInfo(info.toString()); } return info.subject; } /** * Release cache entries got the specified ClassLoader. * * @param classLoader the ClassLoader. */ public void releaseModuleEntries(final ClassLoader classLoader) { if (domainCache != null) { for (Entry<Principal, DomainInfo> entry : domainCache.entrySet()) { if ((classLoader == null && entry.getValue().contextClassLoader == null) || classLoader.equals(entry.getValue().contextClassLoader)) { flushCache(entry.getKey()); } } } } /** * A cache value. Holds information about the authentication process. * * @author <a href="mmoyses@redhat.com">Marcus Moyses</a> */ public static class DomainInfo implements Serializable { private static final long serialVersionUID = 7402775370244483773L; protected LoginContext loginContext; protected Subject subject; protected Object credential; protected Principal callerPrincipal; protected ClassLoader contextClassLoader = null; @Deprecated public void logout() { if (loginContext != null) { try { loginContext.logout(); } catch (Exception e) { PicketBoxLogger.LOGGER.traceCacheEntryLogoutFailure(e); } } } } public void logout(Principal principal, Subject subject) { LoginContext context = null; // if a cache is active, remove the principal from the cache and try to perform the logout using the cached context. if (domainCache != null && principal != null) { if (PicketBoxLogger.LOGGER.isTraceEnabled()) { PicketBoxLogger.LOGGER.traceFlushCacheEntry(principal.getName()); } DomainInfo info = domainCache.get(principal); domainCache.remove(principal); if (info != null && info.loginContext != null) { context = info.loginContext; subject = info.subject; } } // if no cached context was found, create a new one with the incoming subject. if (context == null) { Object[] securityInfo = {principal, null}; CallbackHandler theHandler = null; if (subject == null) subject = new Subject(); try { theHandler = callbackHandler.getClass().newInstance(); setSecurityInfo.invoke(theHandler, securityInfo); context = SubjectActions.createLoginContext(securityDomain, subject, theHandler); } catch (Throwable e) { LoginException le = new LoginException(PicketBoxMessages.MESSAGES.unableToInitializeLoginContext(e)); le.initCause(e); SubjectActions.setContextInfo("org.jboss.security.exception", le); return; } } // perform the JAAS logout. try { context.logout(); if (PicketBoxLogger.LOGGER.isTraceEnabled()) { PicketBoxLogger.LOGGER.traceLogoutSubject(context.toString(), SubjectActions.toString(subject)); } } catch (LoginException le) { SubjectActions.setContextInfo("org.jboss.security.exception", le); } } }