/*
* Copyright 1999-2001,2004 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jboss.web.tomcat.service.sso;
import java.io.IOException;
import java.security.Principal;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import org.apache.catalina.Container;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Manager;
import org.apache.catalina.Realm;
import org.apache.catalina.Session;
import org.apache.catalina.SessionEvent;
import org.apache.catalina.authenticator.Constants;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.session.ManagerBase;
import org.apache.tomcat.util.modeler.Registry;
import org.jboss.web.tomcat.service.session.JBossManager;
import org.jboss.web.tomcat.service.sso.spi.FullyQualifiedSessionId;
import org.jboss.web.tomcat.service.sso.spi.SSOClusterManager;
import org.jboss.web.tomcat.service.sso.spi.SSOCredentials;
import org.jboss.web.tomcat.service.sso.spi.SSOLocalManager;
/**
* 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 propagated 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: 85945 $ $Date: 2009-03-16 15:45:12 -0400 (Mon, 16 Mar 2009) $
*/
public class ClusteredSingleSignOn
extends org.apache.catalina.authenticator.SingleSignOn
implements LifecycleListener, SSOLocalManager
{
/** By default we process expired SSOs no more often than once per minute */
public static final int DEFAULT_PROCESS_EXPIRES_INTERVAL = 60;
/** By default we let SSOs without active sessions live for 30 mins */
public static final int DEFAULT_MAX_EMPTY_LIFE = 1800;
/** The default JBoss Cache to use for storing SSO entries */
public static final String DEFAULT_CACHE_NAME = "clustered-sso";
public static final String LEGACY_CACHE_NAME = "jboss.cache:service=TomcatClusteringCache";
public static final String DEFAULT_CLUSTER_MANAGER_CLASS = "org.jboss.web.tomcat.service.sso.jbc.JBossCacheSSOClusterManager";
// 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 = DEFAULT_CLUSTER_MANAGER_CLASS;
/**
* 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 cacheConfigName = DEFAULT_CACHE_NAME;
/**
* Object name of the thread pool used by SSOClusterManager.
* Only relevant if the SSOClusterManager implementation is
* TreeCacheSSOClusterManager.
*/
private String threadPoolName = "jboss.system:service=ThreadPool";
/** Currently started Managers that have associated as session with an SSO */
private Set activeManagers = Collections.synchronizedSet(new HashSet());
/** Max number of ms an SSO with no active sessions will be usable by a request */
private int maxEmptyLife = DEFAULT_MAX_EMPTY_LIFE * 1000;
/**
* Minimum number of ms since the last processExpires() run
* before a new run is allowed.
*/
private int processExpiresInterval = DEFAULT_PROCESS_EXPIRES_INTERVAL * 1000;
/** Timestamp of the last processExpires() run */
private long lastProcessExpires = System.currentTimeMillis();
/**
* Map<String, Long> containing the ids of SSOs with no active sessions
* and the time at which they entered that state
*/
private Map emptySSOs = new ConcurrentHashMap();
/** Used for sync locking of processExpires runs */
private final Object mutex = new Object();
// ------------------------------------------------------------- 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);
}
}
/**
* Name of the cache config used by SSOClusterManager.
*
* @deprecated use {@link #getCacheConfig()}
*/
@Deprecated
public String getTreeCacheName()
{
return getCacheConfig();
}
/**
* Sets the name of the cache config used by SSOClusterManager.
*
* @deprecated use {@link #setCacheConfig(String)}
*/
@Deprecated
public void setTreeCacheName(String cacheName)
throws Exception
{
setCacheConfig(cacheName);
}
/**
* Name of the cache config used by SSOClusterManager.
*/
public String getCacheConfig()
{
return cacheConfigName;
}
/**
* Sets the name of the cache config used by SSOClusterManager.
*/
public void setCacheConfig(String cacheConfig)
{
this.cacheConfigName = cacheConfig;
}
/**
* Object name of the thread pool used by SSOClusterManager.
* Only relevant if the SSOClusterManager implementation is
* TreeCacheSSOClusterManager.
*/
public String getThreadPoolName()
{
return threadPoolName;
}
/**
* Sets the object name of the thread pool used by SSOClusterManager.
* Only relevant if the SSOClusterManager implementation is
* TreeCacheSSOClusterManager.
*/
public void setThreadPoolName(String poolName)
throws Exception
{
this.threadPoolName = poolName;
}
/**
* Gets the max number of seconds an SSO with no active sessions will be
* usable by a request.
*
* @return a non-negative number
*
* @see #DEFAULT_MAX_EMPTY_LIFE *
* @see #setMaxEmptyLife()
*/
public int getMaxEmptyLife()
{
return (maxEmptyLife / 1000);
}
/**
* Sets the maximum number of seconds an SSO with no active sessions will be
* usable by a request.
* <p>
* A positive value for this property allows a user to continue to use an SSO
* even after all the sessions associated with it have been expired. It does not
* keep an SSO alive if a session associated with it has been invalidated due to
* an <code>HttpSession.invalidate()</code> call.
* </p>
* <p>
* The primary purpose of this property is to avoid the situation where a server
* on which all of an SSO's sessions lives is shutdown, thus expiring all the
* sessions and causing the invalidation of the SSO. A positive value for this
* property would give the user an opportunity to fail over to another server
* and maintain the SSO.
* </p>
*
* @param maxEmptyLife a non-negative number
*
* @throws IllegalArgumentException if <code>maxEmptyLife < 0</code>
*/
public void setMaxEmptyLife(int maxEmptyLife)
{
if (maxEmptyLife < 0)
throw new IllegalArgumentException("maxEmptyLife must be >= 0");
this.maxEmptyLife = maxEmptyLife * 1000;
}
/**
* Gets the minimum number of seconds since the start of the last check for overaged
* SSO's with no active sessions before a new run is allowed.
*
* @return a positive number
*
* @see #DEFAULT_PROCESS_EXPIRES_INTERVAL
* @see #setMaxEmptyLife()
* @see #setProcessExpiresInterval(int)
*/
public int getProcessExpiresInterval()
{
return processExpiresInterval / 1000;
}
/**
* Sets the minimum number of seconds since the start of the last check for overaged
* SSO's with no active sessions before a new run is allowed. During this check,
* any such overaged SSOs will be invalidated.
* <p>
* Note that setting this value does not imply that a check will be performed
* every <code>processExpiresInterval</code> seconds, only that it will not
* be performed more often than that.
* </p>
*
* @param processExpiresInterval a non-negative number. <code>0</code> means
* the overage check can be performed whenever
* the container wishes to.
*
* @throws IllegalArgumentException if <code>processExpiresInterval < 1</code>
*
* @see #setMaxEmptyLife()
*/
public void setProcessExpiresInterval(int processExpiresInterval)
{
if (processExpiresInterval < 0)
throw new IllegalArgumentException("processExpiresInterval must be >= 0");
this.processExpiresInterval = processExpiresInterval * 1000;
}
/**
* Gets the timestamp of the start of the last check for overaged
* SSO's with no active sessions.
*
* @see #setProcessExpiresInterval(int)
*/
public long getLastProcessExpires()
{
return lastProcessExpires;
}
// ------------------------------------------------------ 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)
{
try
{
ssoClusterManager.start();
}
catch (LifecycleException e)
{
throw e;
}
catch (Exception e)
{
throw new LifecycleException("Caught exception stopping " +
ssoClusterManager.getClass().getSimpleName(), e);
}
}
}
/**
* 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)
{
try
{
ssoClusterManager.stop();
}
catch (LifecycleException e)
{
throw e;
}
catch (Exception e)
{
throw new LifecycleException("Caught exception stopping " +
ssoClusterManager.getClass().getSimpleName(), e);
}
}
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().isTraceEnabled())
getContainer().getLogger().trace("Process session destroyed on " + session);
String ssoId = null;
synchronized (reverse)
{
ssoId = (String) reverse.get(session);
}
if (ssoId == null)
return;
try
{
// Was the session destroyed as the result of a timeout or
// the undeployment of the containing webapp?
// 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 (isSessionTimedOut(session) || isManagerStopped(session))
{
removeSession(ssoId, session);
// Quite poor. We hijack the caller thread (the Tomcat background thread)
// to do our cleanup of expired sessions
processExpires();
}
else
{
// The session was logged out.
logout(ssoId);
}
}
catch (Exception e)
{
// Don't propagate back to the webapp; we don't want to disrupt
// the session expiration process
getContainer().getLogger().error("Caught exception updating SSO " + ssoId +
" following destruction of session " +
session.getIdInternal(), e);
}
}
private boolean isSessionTimedOut(Session session)
{
return (session.getMaxInactiveInterval() > 0)
&& (System.currentTimeMillis() - session.getLastAccessedTime() >=
session.getMaxInactiveInterval() * 1000);
}
private boolean isManagerStopped(Session session)
{
boolean stopped = false;
Manager manager = session.getManager();
if (manager instanceof ManagerBase)
{
ObjectName mgrName = ((ManagerBase)manager).getObjectName();
stopped = (!activeManagers.contains(mgrName));
}
else if (manager instanceof JBossManager)
{
ObjectName mgrName = ((JBossManager)manager).getObjectName();
stopped = (!activeManagers.contains(mgrName));
}
else if (manager instanceof Lifecycle)
{
stopped = (!activeManagers.contains(manager));
}
// else we have no way to tell, so assume not
return stopped;
}
// ---------------------------------------------- LifecycleListener Methods
public void lifecycleEvent(LifecycleEvent event)
{
String type = event.getType();
if (Lifecycle.BEFORE_STOP_EVENT.equals(type)
|| Lifecycle.STOP_EVENT.equals(type)
|| Lifecycle.AFTER_STOP_EVENT.equals(type))
{
Lifecycle source = event.getLifecycle();
boolean removed;
if (source instanceof ManagerBase)
{
removed = activeManagers.remove(((ManagerBase)source).getObjectName());
}
else if (source instanceof JBossManager)
{
removed = activeManagers.remove(((JBossManager)source).getObjectName());
}
else
{
removed = activeManagers.remove(source);
}
if (removed)
{
source.removeLifecycleListener(this);
getContainer().getLogger().debug("ClusteredSSO: removed stopped " +
"manager " + source.toString());
}
// TODO consider getting the sessions and removing any from our sso's
// Idea is to cleanup after managers that don't destroy sessions
}
}
// ---------------------------------------------------------- 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().isTraceEnabled())
getContainer().getLogger().trace("Process request for '" + request.getRequestURI() + "'");
if (request.getUserPrincipal() != null)
{
if (getContainer().getLogger().isTraceEnabled())
getContainer().getLogger().trace(" 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().isTraceEnabled())
getContainer().getLogger().trace(" SSO cookie is not present");
getNext().invoke(request, response);
return;
}
// Look up the cached Principal associated with this cookie value
String ssoId = cookie.getValue();
if (getContainer().getLogger().isTraceEnabled())
getContainer().getLogger().trace(" Checking for cached principal for " + ssoId);
JBossSingleSignOnEntry entry = getSingleSignOnEntry(cookie.getValue());
if (entry != null && isValid(ssoId, entry))
{
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().isTraceEnabled())
{
getContainer().getLogger().trace(" 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().isTraceEnabled())
getContainer().getLogger().trace(" 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
*/
public void associate(String ssoId, Session session)
{
if (getContainer().getLogger().isTraceEnabled())
getContainer().getLogger().trace("Associate sso id " + ssoId + " with session " + session);
JBossSingleSignOnEntry sso = getSingleSignOnEntry(ssoId);
boolean added = false;
if (sso != null)
added = sso.addSession2(this, session);
synchronized (reverse)
{
reverse.put(session, ssoId);
}
// If we made a change, track the manager and notify any cluster
if (added)
{
Manager manager = session.getManager();
// Prefer to cache an ObjectName to avoid risk of leaking a manager,
// so if the manager exposes one, use it
Object mgrKey = null;
if (manager instanceof ManagerBase)
{
mgrKey = ((ManagerBase)manager).getObjectName();
}
else if (manager instanceof JBossManager)
{
mgrKey = ((JBossManager)manager).getObjectName();
}
else if (manager instanceof Lifecycle)
{
mgrKey = manager;
}
else {
getContainer().getLogger().warn("Manager for session " +
session.getIdInternal() +
" does not implement Lifecycle; web app shutdown may " +
" lead to incorrect SSO invalidations");
}
if (mgrKey != null)
{
synchronized (activeManagers)
{
if (!activeManagers.contains(mgrKey))
{
activeManagers.add(mgrKey);
((Lifecycle) manager).addLifecycleListener(this);
}
}
}
if (ssoClusterManager != null)
ssoClusterManager.addSession(ssoId, getFullyQualifiedSessionId(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);
}
JBossSingleSignOnEntry sso = getSingleSignOnEntry(ssoId);
if (sso == null)
return;
boolean removed = sso.removeSession2(session);
// If we changed anything, notify any cluster
if (removed && ssoClusterManager != null)
{
ssoClusterManager.removeSession(ssoId, getFullyQualifiedSessionId(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 = (JBossSingleSignOnEntry) cache.remove(ssoId);
}
}
}
/**
* Deregister the specified single sign on identifier, and invalidate
* any associated sessions.
*
* @param ssoId Single sign on identifier to deregister
*/
public void deregister(String ssoId)
{
if (getContainer().getLogger().isTraceEnabled())
getContainer().getLogger().trace("Deregistering sso id '" + ssoId + "'");
// It's possible we don't have the SSO locally but it's in
// the emptySSOs map; if so remove it
emptySSOs.remove(ssoId);
// Look up and remove the corresponding SingleSignOnEntry
JBossSingleSignOnEntry sso = null;
synchronized (cache)
{
sso = (JBossSingleSignOnEntry) 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 JBossSingleSignOnEntry getSingleSignOnEntry(String ssoId)
{
JBossSingleSignOnEntry 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)
{
SSOCredentials credentials = ssoClusterManager.lookup(ssoId);
if (credentials != null)
{
sso = new JBossSingleSignOnEntry(null, credentials.getAuthType(),
credentials.getUsername(),
credentials.getPassword());
// 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.
*/
public boolean reauthenticate(String ssoId, Realm realm,
Request request)
{
if (ssoId == null || realm == null)
return false;
boolean reauthenticated = false;
JBossSingleSignOnEntry 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);
// JBAS-2314 -- bind principal to the entry as well
entry.setPrincipal(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
*/
public 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().isTraceEnabled())
getContainer().getLogger().trace("Removing session " + session.toString() +
" from sso id " + ssoId);
// Get a reference to the SingleSignOn
JBossSingleSignOnEntry entry = getSingleSignOnEntry(ssoId);
if (entry == null)
return;
// Remove the inactive session from SingleSignOnEntry
boolean removed = entry.removeSession2(session);
// If we changed anything, notify any cluster
if (removed && ssoClusterManager != null)
{
ssoClusterManager.removeSession(ssoId, getFullyQualifiedSessionId(session));
}
// Remove the inactive session from the 'reverse' Map.
synchronized (reverse)
{
reverse.remove(session);
}
}
/**
* 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
*/
public 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.
*/
JBossSingleSignOnEntry localLookup(String ssoId)
{
synchronized (cache)
{
return ((JBossSingleSignOnEntry) 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().isTraceEnabled())
{
getContainer().getLogger().trace("Registering sso id '" + ssoId + "' for user '" +
principal.getName() + "' with auth type '" + authType + "'");
}
synchronized (cache)
{
cache.put(ssoId, new JBossSingleSignOnEntry(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;
JBossSingleSignOnEntry sso = getSingleSignOnEntry(ssoId);
// Only update if the entry is missing information
if (sso != null)
{
if (sso.getCanReauthenticate() == false)
{
if (getContainer().getLogger().isTraceEnabled())
getContainer().getLogger().trace("Update sso id " + ssoId + " to auth type " + authType);
synchronized (sso)
{
shouldBroadcast = sso.updateCredentials2(principal, authType,
username, password);
}
}
else if (sso.getPrincipal() == null && principal != null)
{
if (getContainer().getLogger().isTraceEnabled())
getContainer().getLogger().trace("Update sso id " + ssoId + " with principal " +
principal.getName());
synchronized (sso)
{
sso.setPrincipal(principal);
// No need to notify cluster; Principals don't replicate
}
}
}
return shouldBroadcast;
}
public void remoteUpdate(String ssoId, SSOCredentials credentials)
{
JBossSingleSignOnEntry sso = localLookup(ssoId);
// Only update if the entry is missing information
if (sso != null && sso.getCanReauthenticate() == false)
{
if (getContainer().getLogger().isTraceEnabled())
getContainer().getLogger().trace("Update sso id " + ssoId +
" to auth type " + credentials.getAuthType());
synchronized (sso)
{
// Use the existing principal
Principal p = sso.getPrincipal();
sso.updateCredentials(p, credentials.getAuthType(),
credentials.getUsername(),
credentials.getPassword());
}
}
}
/**
* Callback from the SSOManager when it detects an SSO without
* any active sessions across the cluster
*/
public void notifySSOEmpty(String ssoId)
{
Object obj = emptySSOs.put(ssoId, new Long(System.currentTimeMillis()));
if (obj == null && getContainer().getLogger().isTraceEnabled())
{
getContainer().getLogger().trace("Notified that SSO " + ssoId + " is empty");
}
}
/**
* Callback from the SSOManager when it detects an SSO that
* has active sessions across the cluster
*/
public void notifySSONotEmpty(String ssoId)
{
Object obj = emptySSOs.remove(ssoId);
if (obj != null && getContainer().getLogger().isTraceEnabled())
{
getContainer().getLogger().trace("Notified that SSO " + ssoId +
" is no longer empty");
}
}
/**
* Get the current Catalina MBean Server.
*
* @return the mbean server
*/
public MBeanServer getMBeanServer()
{
if (mserver == null)
{
mserver = Registry.getRegistry(null, null).getMBeanServer();
}
return mserver;
}
// ------------------------------------------------------- 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.setSSOLocalManager(this);
ssoClusterManager = mgr;
clusterManagerClass = className;
}
catch (Throwable t)
{
throw new LifecycleException("Cannot create " +
"SSOClusterManager using " +
className, t);
}
if (started)
{
try
{
ssoClusterManager.start();
}
catch (LifecycleException e)
{
throw e;
}
catch (Exception e)
{
throw new LifecycleException("Caught exception stopping " +
ssoClusterManager.getClass().getSimpleName(), e);
}
}
}
}
private void processExpires()
{
long now = 0L;
synchronized (mutex)
{
now = System.currentTimeMillis();
if (now - lastProcessExpires > processExpiresInterval)
{
lastProcessExpires = now;
}
else
{
return;
}
}
clearExpiredSSOs(now);
}
private synchronized void clearExpiredSSOs(long now)
{
for (Iterator iter = emptySSOs.entrySet().iterator(); iter.hasNext();)
{
Map.Entry entry = (Map.Entry) iter.next();
if ( (now - ((Long) entry.getValue()).longValue()) > maxEmptyLife)
{
String ssoId = (String) entry.getKey();
if (getContainer().getLogger().isTraceEnabled())
{
getContainer().getLogger().trace("Invalidating expired SSO " + ssoId);
}
logout(ssoId);
}
}
}
private boolean isValid(String ssoId, JBossSingleSignOnEntry entry)
{
boolean valid = true;
if (entry.getSessionCount() == 0)
{
Long expired = (Long) emptySSOs.get(ssoId);
if (expired != null
&& (System.currentTimeMillis() - expired.longValue()) > maxEmptyLife)
{
valid = false;
if (getContainer().getLogger().isTraceEnabled())
{
getContainer().getLogger().trace("Invalidating expired SSO " + ssoId);
}
logout(ssoId);
}
}
return valid;
}
private FullyQualifiedSessionId getFullyQualifiedSessionId(Session session)
{
String id = session.getIdInternal();
Container context = session.getManager().getContainer();
String contextName = context.getName();
Container host = context.getParent();
String hostName = host.getName();
return new FullyQualifiedSessionId(id, contextName, hostName);
}
}