ClusteredSingleSignOn.java |
/* * JBoss, the OpenSource WebOS * * Distributable under LGPL license. * See terms of license at gnu.org. */ package org.jboss.web.tomcat.tc5.sso; import org.jboss.web.tomcat.tc5.Tomcat5; import java.io.IOException; import java.security.Principal; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import org.apache.catalina.LifecycleException; import org.apache.catalina.Session; import org.apache.catalina.Realm; import org.apache.catalina.SessionEvent; import org.apache.catalina.authenticator.Constants; import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; /** * A <strong>Valve</strong> that supports a "single sign on" user experience, * where the security identity of a user who successfully authenticates to one * web application is propogated to other web applications in the same * security domain. For successful use, the following requirements must * be met: * <ul> * <li>This Valve must be configured on the Container that represents a * virtual host (typically an implementation of <code>Host</code>).</li> * <li>The <code>Realm</code> that contains the shared user and role * information must be configured on the same Container (or a higher * one), and not overridden at the web application level.</li> * <li>The web applications themselves must use one of the standard * Authenticators found in the * <code>org.apache.catalina.authenticator</code> package.</li> * </ul> * * @author Brian E. Stansberry based on the work of Craig R. McClanahan * @version $Revision: 1.4.2.1 $ $Date: 2005/01/28 12:30:49 $ */ public class ClusteredSingleSignOn extends org.apache.catalina.authenticator.SingleSignOn { // Override the superclass value static { info = ClusteredSingleSignOn.class.getName(); } // ----------------------------------------------------- Instance Variables /** * Fully qualified name of a class implementing * {@link SSOClusterManager SSOClusterManager} that will be used * to manage SSOs across a cluster. */ private String clusterManagerClass = TreeCacheSSOClusterManager.class.getName(); /** * Object used to provide cross-cluster support for single sign on. */ private SSOClusterManager ssoClusterManager = null; /** * Object name of the tree cache used by SSOClusterManager. * Only relevant if the SSOClusterManager implementation is * TreeCacheSSOClusterManager. */ private String treeCacheName = Tomcat5.DEFAULT_CACHE_NAME; // ------------------------------------------------------------- Properties /** * Gets the object that provides SSO support across a cluster. * * @return the object provided cluster support, or <code>null</code> if * no such object has been configured. */ public SSOClusterManager getClusterManager() { return this.ssoClusterManager; } /** * Sets the object that provides SSO support across a cluster. * * @param clusterManager the object that provides SSO support. * @throws IllegalStateException if this method is invoked after this valve * has been started. */ public void setClusterManager(SSOClusterManager clusterManager) { if (started && (clusterManager != ssoClusterManager)) { throw new IllegalStateException("already started -- cannot set a " + "new SSOClusterManager"); } this.ssoClusterManager = clusterManager; if (clusterManager != null) { clusterManagerClass = clusterManager.getClass().getName(); } } /** * Gets the name of the class that will be used to provide SSO support * across a cluster. * * @return Fully qualified name of a class implementing * {@link SSOClusterManager SSOClusterManager} * that is being used to manage SSOs across a cluster. * May return <code>null</code> (the default) if clustered * SSO support is not configured. */ public String getClusterManagerClass() { return clusterManagerClass; } /** * Sets the name of the class that will be used to provide SSO support * across a cluster. * <p><b>NOTE: </b> * If this Valve has already started, and no SSOClusterManager has been * configured for it, calling this method will * * @param managerClass Fully qualified name of a class implementing * {@link SSOClusterManager SSOClusterManager} * that will be used to manage SSOs across a cluster. * Class must declare a public no-arguments * constructor. <code>null</code> is allowed. */ public void setClusterManagerClass(String managerClass) { if (!started) { clusterManagerClass = managerClass; } else if (ssoClusterManager == null) { try { createClusterManager(managerClass); } catch (LifecycleException e) { getContainer().getLogger().error("Exception creating SSOClusterManager " + managerClass, e); } } else { getContainer().getLogger().error("Cannot set clusterManagerClass to " + managerClass + "; already started using " + clusterManagerClass); } } /** * Object name of the tree cache used by SSOClusterManager. * Only relevant if the SSOClusterManager implementation is * TreeCacheSSOClusterManager. */ public String getTreeCacheName() { return treeCacheName; } /** * Sets the object name of the tree cache used by SSOClusterManager. * Only relevant if the SSOClusterManager implementation is * TreeCacheSSOClusterManager. */ public void setTreeCacheName(String cacheName) throws Exception { this.treeCacheName = cacheName; if (ssoClusterManager != null && ssoClusterManager instanceof TreeCacheSSOClusterManager) { ((TreeCacheSSOClusterManager) ssoClusterManager).setCacheName(cacheName); } } // ------------------------------------------------------ Lifecycle Methods /** * Prepare for the beginning of active use of the public methods of this * component. This method should be called after <code>configure()</code>, * and before any of the public methods of the component are utilized. * * @throws LifecycleException if this component detects a fatal error * that prevents this component from being used */ public void start() throws LifecycleException { // Validate and update our current component state if (started) { throw new LifecycleException (sm.getString("authenticator.alreadyStarted")); } // Attempt to create an SSOClusterManager createClusterManager(getClusterManagerClass()); lifecycle.fireLifecycleEvent(START_EVENT, null); started = true; if (ssoClusterManager != null) { ssoClusterManager.start(); } } /** * Gracefully terminate the active use of the public methods of this * component. This method should be the last one called on a given * instance of this component. * * @throws LifecycleException if this component detects a fatal error * that needs to be reported */ public void stop() throws LifecycleException { // Validate and update our current component state if (!started) { throw new LifecycleException (sm.getString("authenticator.notStarted")); } if (ssoClusterManager != null) { ssoClusterManager.stop(); } lifecycle.fireLifecycleEvent(STOP_EVENT, null); started = false; } // ------------------------------------------------ SessionListener Methods /** * Updates the state of a single sign on session to reflect the destruction * of a standard HTTP session. * <p/> * If the given event is a {@link Session#SESSION_DESTROYED_EVENT * Session destroyed event}, checks whether the session was destroyed due * to timeout or user action (i.e. logout). If due to timeout, disassociates * the Session from the single sign on session. If due to logout, invokes * the {@link #logout} method. * * @param event SessionEvent that has occurred */ public void sessionEvent(SessionEvent event) { // We only care about session destroyed events if (!Session.SESSION_DESTROYED_EVENT.equals(event.getType())) return; // Look up the single session id associated with this session (if any) Session session = event.getSession(); if (getContainer().getLogger().isDebugEnabled()) getContainer().getLogger().debug("Process session destroyed on " + session); String ssoId = null; synchronized (reverse) { ssoId = (String) reverse.get(session); } if (ssoId == null) return; // Was the session destroyed as the result of a timeout? // If so, we'll just remove the expired session from the // SSO. If the session was logged out, we'll log out // of all sessions associated with the SSO. if ((session.getMaxInactiveInterval() > 0) && (System.currentTimeMillis() - session.getLastAccessedTime() >= session.getMaxInactiveInterval() * 1000)) { removeSession(ssoId, session); } else { // The session was logged out. logout(ssoId); } } // ---------------------------------------------------------- Valve Methods /** * Perform single-sign-on support processing for this request. * <p/> * Overrides the superclass version by handling the fact that a * single sign on may have been originated on another cluster node and * thus may not have a <code>Principal</code> object associated with it * on this node. * * @param request The servlet request we are processing * @param response The servlet response we are creating * @param context The valve context used to invoke the next valve * in the current processing pipeline * @throws IOException if an input/output error occurs * @throws ServletException if a servlet error occurs */ public void invoke(Request request, Response response) throws IOException, ServletException { request.removeNote(Constants.REQ_SSOID_NOTE); // Has a valid user already been authenticated? if (getContainer().getLogger().isDebugEnabled()) getContainer().getLogger().debug("Process request for '" + request.getRequestURI() + "'"); if (request.getUserPrincipal() != null) { if (getContainer().getLogger().isDebugEnabled()) getContainer().getLogger().debug(" Principal '" + request.getUserPrincipal().getName() + "' has already been authenticated"); getNext().invoke(request, response); return; } // Check for the single sign on cookie Cookie cookie = null; Cookie cookies[] = request.getCookies(); if (cookies == null) cookies = new Cookie[0]; for (int i = 0; i < cookies.length; i++) { if (Constants.SINGLE_SIGN_ON_COOKIE.equals(cookies[i].getName())) { cookie = cookies[i]; break; } } if (cookie == null) { if (getContainer().getLogger().isDebugEnabled()) getContainer().getLogger().debug(" SSO cookie is not present"); getNext().invoke(request, response); return; } // Look up the cached Principal associated with this cookie value if (getContainer().getLogger().isDebugEnabled()) getContainer().getLogger().debug(" Checking for cached principal for " + cookie.getValue()); SingleSignOnEntry entry = getSingleSignOnEntry(cookie.getValue()); if (entry != null) { Principal ssoPrinc = entry.getPrincipal(); // have to deal with the fact that the entry may not have an // associated Principal. SSO entries retrieved via a lookup from a // cluster will not have a Principal, as Principal is not Serializable if (getContainer().getLogger().isDebugEnabled()) { getContainer().getLogger().debug(" Found cached principal '" + (ssoPrinc == null ? "NULL" : ssoPrinc.getName()) + "' with auth type '" + entry.getAuthType() + "'"); } request.setNote(Constants.REQ_SSOID_NOTE, cookie.getValue()); // Only set security elements if per-request reauthentication is // not required AND the SSO entry had a Principal. if (!getRequireReauthentication() && ssoPrinc != null) { request.setAuthType(entry.getAuthType()); request.setUserPrincipal(ssoPrinc); } } else { if (getContainer().getLogger().isDebugEnabled()) getContainer().getLogger().debug(" No cached principal found, erasing SSO cookie"); cookie.setMaxAge(0); response.addCookie(cookie); } // Invoke the next Valve in our pipeline getNext().invoke(request, response); } // ------------------------------------------------------ Protected Methods /** * Associate the specified single sign on identifier with the * specified Session. * <p/> * Differs from the superclass version in that it notifies the cluster * of any new association of SSO and Session. * * @param ssoId Single sign on identifier * @param session Session to be associated */ protected void associate(String ssoId, Session session) { if (getContainer().getLogger().isDebugEnabled()) getContainer().getLogger().debug("Associate sso id " + ssoId + " with session " + session); SingleSignOnEntry sso = getSingleSignOnEntry(ssoId); boolean added = false; if (sso != null) added = sso.addSession(this, session); synchronized (reverse) { reverse.put(session, ssoId); } // If we made a change, notify any cluster if (added && ssoClusterManager != null) ssoClusterManager.addSession(ssoId, session); } /** * Deregister the specified session. If it is the last session, * then also get rid of the single sign on identifier. * <p/> * Differs from the superclass version in that it notifies the cluster * of any disassociation of SSO and Session. * * @param ssoId Single sign on identifier * @param session Session to be deregistered */ protected void deregister(String ssoId, Session session) { synchronized (reverse) { reverse.remove(session); } SingleSignOnEntry sso = getSingleSignOnEntry(ssoId); if (sso == null) return; boolean removed = sso.removeSession(session); // If we changed anything, notify any cluster if (removed && ssoClusterManager != null) { ssoClusterManager.removeSession(ssoId, session); } // see if this was the last session on this node, // if remove sso entry from our local cache if (sso.getSessionCount() == 0) { synchronized (cache) { sso = (SingleSignOnEntry) cache.remove(ssoId); } } } /** * Deregister the specified single sign on identifier, and invalidate * any associated sessions. * * @param ssoId Single sign on identifier to deregister */ protected void deregister(String ssoId) { if (getContainer().getLogger().isDebugEnabled()) getContainer().getLogger().debug("Deregistering sso id '" + ssoId + "'"); // Look up and remove the corresponding SingleSignOnEntry SingleSignOnEntry sso = null; synchronized (cache) { sso = (SingleSignOnEntry) cache.remove(ssoId); } if (sso == null) return; // Expire any associated sessions Session sessions[] = sso.findSessions(); for (int i = 0; i < sessions.length; i++) { if (getContainer().getLogger().isTraceEnabled()) getContainer().getLogger().trace(" Invalidating session " + sessions[i]); // Remove from reverse cache first to avoid recursion synchronized (reverse) { reverse.remove(sessions[i]); } // Invalidate this session sessions[i].expire(); } // NOTE: Clients may still possess the old single sign on cookie, // but it will be removed on the next request since it is no longer // in the cache } /** * Deregister the given SSO, invalidating any associated sessions, then * notify any cluster of the logout. * * @param ssoId the id of the SSO session */ protected void logout(String ssoId) { deregister(ssoId); // broadcast logout to any cluster if (ssoClusterManager != null) ssoClusterManager.logout(ssoId); } /** * Look up and return the cached SingleSignOn entry associated with this * sso id value, if there is one; otherwise return <code>null</code>. * * @param ssoId Single sign on identifier to look up */ protected SingleSignOnEntry getSingleSignOnEntry(String ssoId) { SingleSignOnEntry sso = localLookup(ssoId); // If we don't have one locally and there is a cluster, // query the cluster for the SSO if (sso == null && ssoClusterManager != null) { sso = ssoClusterManager.lookup(ssoId); if (sso != null) { // Store it locally synchronized (cache) { cache.put(ssoId, sso); } } } return sso; } /** * Attempts reauthentication to the given <code>Realm</code> using * the credentials associated with the single sign-on session * identified by argument <code>ssoId</code>. * <p/> * If reauthentication is successful, the <code>Principal</code> and * authorization type associated with the SSO session will be bound * to the given <code>HttpRequest</code> object via calls to * {@link HttpRequest#setAuthType HttpRequest.setAuthType()} and * {@link HttpRequest#setUserPrincipal HttpRequest.setUserPrincipal()} * </p> * * @param ssoId identifier of SingleSignOn session with which the * caller is associated * @param realm Realm implementation against which the caller is to * be authenticated * @param request the request that needs to be authenticated * @return <code>true</code> if reauthentication was successful, * <code>false</code> otherwise. */ protected boolean reauthenticate(String ssoId, Realm realm, Request request) { if (ssoId == null || realm == null) return false; boolean reauthenticated = false; SingleSignOnEntry entry = getSingleSignOnEntry(ssoId); if (entry != null && entry.getCanReauthenticate()) { String username = entry.getUsername(); if (username != null) { Principal reauthPrincipal = realm.authenticate(username, entry.getPassword()); if (reauthPrincipal != null) { reauthenticated = true; // Bind the authorization credentials to the request request.setAuthType(entry.getAuthType()); request.setUserPrincipal(reauthPrincipal); } } } return reauthenticated; } /** * Register the specified Principal as being associated with the specified * value for the single sign on identifier. * <p/> * Differs from the superclass version in that it notifies the cluster * of the registration. * * @param ssoId Single sign on identifier to register * @param principal Associated user principal that is identified * @param authType Authentication type used to authenticate this * user principal * @param username Username used to authenticate this user * @param password Password used to authenticate this user */ protected void register(String ssoId, Principal principal, String authType, String username, String password) { registerLocal(ssoId, principal, authType, username, password); // broadcast change to any cluster if (ssoClusterManager != null) ssoClusterManager.register(ssoId, authType, username, password); } /** * Remove a single Session from a SingleSignOn. Called when * a session is timed out and no longer active. * <p/> * Differs from the superclass version in that it notifies the cluster * of any disassociation of SSO and Session. * * @param ssoId Single sign on identifier from which to remove the session. * @param session the session to be removed. */ protected void removeSession(String ssoId, Session session) { if (getContainer().getLogger().isDebugEnabled()) getContainer().getLogger().debug("Removing session " + session.toString() + " from sso id " + ssoId); // Get a reference to the SingleSignOn SingleSignOnEntry entry = getSingleSignOnEntry(ssoId); if (entry == null) return; // Remove the inactive session from SingleSignOnEntry boolean removed = entry.removeSession(session); // If we changed anything, notify any cluster if (removed && ssoClusterManager != null) { ssoClusterManager.removeSession(ssoId, session); } // Remove the inactive session from the 'reverse' Map. synchronized (reverse) { reverse.remove(session); } // If there are no sessions left in the SingleSignOnEntry, // deregister the entry. if (entry.getSessionCount() == 0) { deregister(ssoId); } } /** * Updates any <code>SingleSignOnEntry</code> found under key * <code>ssoId</code> with the given authentication data. * <p/> * The purpose of this method is to allow an SSO entry that was * established without a username/password combination (i.e. established * following DIGEST or CLIENT-CERT authentication) to be updated with * a username and password if one becomes available through a subsequent * BASIC or FORM authentication. The SSO entry will then be usable for * reauthentication. * <p/> * <b>NOTE:</b> Only updates the SSO entry if a call to * <code>SingleSignOnEntry.getCanReauthenticate()</code> returns * <code>false</code>; otherwise, it is assumed that the SSO entry already * has sufficient information to allow reauthentication and that no update * is needed. * <p/> * Differs from the superclass version in that it notifies the cluster * of any update. * * @param ssoId identifier of Single sign to be updated * @param principal the <code>Principal</code> returned by the latest * call to <code>Realm.authenticate</code>. * @param authType the type of authenticator used (BASIC, CLIENT-CERT, * DIGEST or FORM) * @param username the username (if any) used for the authentication * @param password the password (if any) used for the authentication */ protected void update(String ssoId, Principal principal, String authType, String username, String password) { boolean needToBroadcast = updateLocal(ssoId, principal, authType, username, password); // if there was a change, broadcast it to any cluster if (needToBroadcast && ssoClusterManager != null) { ssoClusterManager.updateCredentials(ssoId, authType, username, password); } } //---------------------------------------------- Package-Protected Methods /** * Search in our local cache for an SSO entry. * * @param ssoId the id of the SSO session * @return any SingleSignOnEntry associated with the given id, or * <code>null</code> if there is none. */ SingleSignOnEntry localLookup(String ssoId) { synchronized (cache) { return ((SingleSignOnEntry) cache.get(ssoId)); } } /** * Create a SingleSignOnEntry using the passed configuration parameters and * register it in the local cache, bound to the given id. * * @param ssoId the id of the SSO session * @param principal the <code>Principal</code> returned by the latest * call to <code>Realm.authenticate</code>. * @param authType the type of authenticator used (BASIC, CLIENT-CERT, * DIGEST or FORM) * @param username the username (if any) used for the authentication * @param password the password (if any) used for the authentication */ void registerLocal(String ssoId, Principal principal, String authType, String username, String password) { if (getContainer().getLogger().isDebugEnabled()) { getContainer().getLogger().debug("Registering sso id '" + ssoId + "' for user '" + principal.getName() + "' with auth type '" + authType + "'"); } synchronized (cache) { cache.put(ssoId, new SingleSignOnEntry(principal, authType, username, password)); } } /** * Updates any <code>SingleSignOnEntry</code> found under key * <code>ssoId</code> with the given authentication data. * * @param ssoId identifier of Single sign to be updated * @param principal the <code>Principal</code> returned by the latest * call to <code>Realm.authenticate</code>. * @param authType the type of authenticator used (BASIC, CLIENT-CERT, * DIGEST or FORM) * @param username the username (if any) used for the authentication * @param password the password (if any) used for the authentication * @return <code>true</code> if the update resulted in an actual change * to the entry's authType, username or principal properties */ boolean updateLocal(String ssoId, Principal principal, String authType, String username, String password) { boolean shouldBroadcast = false; SingleSignOnEntry sso = getSingleSignOnEntry(ssoId); // Only update if the entry is missing information if (sso != null) { if (sso.getCanReauthenticate() == false) { if (getContainer().getLogger().isDebugEnabled()) getContainer().getLogger().debug("Update sso id " + ssoId + " to auth type " + authType); synchronized (sso) { shouldBroadcast = sso.updateCredentials(principal, authType, username, password); } } else if (sso.getPrincipal() == null && principal != null) { if (getContainer().getLogger().isDebugEnabled()) getContainer().getLogger().debug("Update sso id " + ssoId + " with principal " + principal.getName()); synchronized (sso) { sso.setPrincipal(principal); // No need to notify cluster; Principals don't replicate } } } return shouldBroadcast; } void remoteUpdate(String ssoId, String authType, String username, String password) { SingleSignOnEntry sso = getSingleSignOnEntry(ssoId); // Only update if the entry is missing information if (sso != null && sso.getCanReauthenticate() == false) { if (getContainer().getLogger().isDebugEnabled()) getContainer().getLogger().debug("Update sso id " + ssoId + " to auth type " + authType); synchronized (sso) { // Use the existing principal Principal p = sso.getPrincipal(); sso.updateCredentials(p, authType, username, password); } } } // ------------------------------------------------------- Private Methods /** * Instantiates an instance of the given class, making it this valve's * SSOClusterManager. * <p/> * If this valve has been started and the given class implements * <code>Lifecycle</code>, starts the new SSOClusterManager. * * @param className fully qualified class name of an implementation * of {@link SSOClusterManager SSOClusterManager}. * @throws LifecycleException if there is any problem instantiating or * starting the object, or if the created * object does not implement * <code>SSOClusterManger</code> */ private void createClusterManager(String className) throws LifecycleException { if (ssoClusterManager != null) return; if (className != null) { SSOClusterManager mgr = null; try { ClassLoader tcl = Thread.currentThread().getContextClassLoader(); Class clazz = tcl.loadClass(className); mgr = (SSOClusterManager) clazz.newInstance(); mgr.setSingleSignOnValve(this); if (mgr instanceof TreeCacheSSOClusterManager) { ((TreeCacheSSOClusterManager) mgr).setCacheName(getTreeCacheName()); } ssoClusterManager = mgr; clusterManagerClass = className; } catch (Throwable t) { throw new LifecycleException("Cannot create " + "SSOClusterManager using " + className, t); } if (started) { ssoClusterManager.start(); } } } }
ClusteredSingleSignOn.java |