/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.catalina.authenticator;

import java.io.IOException;
import java.security.Principal;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;

import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Engine;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.Manager;
import org.apache.catalina.Realm;
import org.apache.catalina.Session;
import org.apache.catalina.SessionListener;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
import org.apache.tomcat.util.res.StringManager;

A Valve 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:
  • This Valve must be configured on the Container that represents a virtual host (typically an implementation of Host).
  • The Realm 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.
  • The web applications themselves must use one of the standard Authenticators found in the org.apache.catalina.authenticator package.
Author:Craig R. McClanahan
/** * 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 Craig R. McClanahan */
public class SingleSignOn extends ValveBase { private static final StringManager sm = StringManager.getManager(SingleSignOn.class); /* The engine at the top of the container hierarchy in which this SSO Valve * has been placed. It is used to get back to a session object from a * SingleSignOnSessionKey and is updated when the Valve starts and stops. */ private Engine engine; //------------------------------------------------------ Constructor public SingleSignOn() { super(true); } // ----------------------------------------------------- Instance Variables
The cache of SingleSignOnEntry instances for authenticated Principals, keyed by the cookie value that is used to select them.
/** * The cache of SingleSignOnEntry instances for authenticated Principals, * keyed by the cookie value that is used to select them. */
protected Map<String,SingleSignOnEntry> cache = new ConcurrentHashMap<>();
Indicates whether this valve should require a downstream Authenticator to reauthenticate each request, or if it itself can bind a UserPrincipal and AuthType object to the request.
/** * Indicates whether this valve should require a downstream Authenticator to * reauthenticate each request, or if it itself can bind a UserPrincipal * and AuthType object to the request. */
private boolean requireReauthentication = false;
Optional SSO cookie domain.
/** * Optional SSO cookie domain. */
private String cookieDomain;
SSO cookie name, the default value is JSESSIONIDSSO.
/** * SSO cookie name, the default value is <code>JSESSIONIDSSO</code>. */
private String cookieName = Constants.SINGLE_SIGN_ON_COOKIE; // ------------------------------------------------------------- Properties
Returns the optional cookie domain. May return null.
Returns:The cookie domain
/** * Returns the optional cookie domain. * May return null. * * @return The cookie domain */
public String getCookieDomain() { return cookieDomain; }
Sets the domain to be used for sso cookies.
Params:
  • cookieDomain – cookie domain name
/** * Sets the domain to be used for sso cookies. * * @param cookieDomain cookie domain name */
public void setCookieDomain(String cookieDomain) { if (cookieDomain != null && cookieDomain.trim().length() == 0) { this.cookieDomain = null; } else { this.cookieDomain = cookieDomain; } }
Returns:the cookie name
/** * @return the cookie name */
public String getCookieName() { return cookieName; }
Set the cookie name that will be used for the SSO cookie.
Params:
  • cookieName – the cookieName to set
/** * Set the cookie name that will be used for the SSO cookie. * @param cookieName the cookieName to set */
public void setCookieName(String cookieName) { this.cookieName = cookieName; }
Gets whether each request needs to be reauthenticated (by an Authenticator downstream in the pipeline) to the security Realm, or if this Valve can itself bind security info to the request based on the presence of a valid SSO entry without rechecking with the Realm.
See Also:
Returns: true if it is required that a downstream Authenticator reauthenticate each request before calls to HttpServletRequest.setUserPrincipal() and HttpServletRequest.setAuthType() are made; false if the Valve can itself make those calls relying on the presence of a valid SingleSignOn entry associated with the request.
/** * Gets whether each request needs to be reauthenticated (by an * Authenticator downstream in the pipeline) to the security * <code>Realm</code>, or if this Valve can itself bind security info * to the request based on the presence of a valid SSO entry without * rechecking with the <code>Realm</code>. * * @return <code>true</code> if it is required that a downstream * Authenticator reauthenticate each request before calls to * <code>HttpServletRequest.setUserPrincipal()</code> * and <code>HttpServletRequest.setAuthType()</code> are made; * <code>false</code> if the <code>Valve</code> can itself make * those calls relying on the presence of a valid SingleSignOn * entry associated with the request. * * @see #setRequireReauthentication */
public boolean getRequireReauthentication() { return requireReauthentication; }
Sets whether each request needs to be reauthenticated (by an Authenticator downstream in the pipeline) to the security Realm, or if this Valve can itself bind security info to the request, based on the presence of a valid SSO entry, without rechecking with the Realm.

If this property is false (the default), this Valve will bind a UserPrincipal and AuthType to the request if a valid SSO entry is associated with the request. It will not notify the security Realm of the incoming request.

This property should be set to true if the overall server configuration requires that the Realm reauthenticate each request thread. An example of such a configuration would be one where the Realm implementation provides security for both a web tier and an associated EJB tier, and needs to set security credentials on each request thread in order to support EJB access.

If this property is set to true, this Valve will set flags on the request notifying the downstream Authenticator that the request is associated with an SSO session. The Authenticator will then call its reauthenticateFromSSO method to attempt to reauthenticate the request to the Realm, using any credentials that were cached with this Valve.

The default value of this property is false, in order to maintain backward compatibility with previous versions of Tomcat.

Params:
  • required – true if it is required that a downstream Authenticator reauthenticate each request before calls to HttpServletRequest.setUserPrincipal() and HttpServletRequest.setAuthType() are made; false if the Valve can itself make those calls relying on the presence of a valid SingleSignOn entry associated with the request.
See Also:
/** * Sets whether each request needs to be reauthenticated (by an * Authenticator downstream in the pipeline) to the security * <code>Realm</code>, or if this Valve can itself bind security info * to the request, based on the presence of a valid SSO entry, without * rechecking with the <code>Realm</code>. * <p> * If this property is <code>false</code> (the default), this * <code>Valve</code> will bind a UserPrincipal and AuthType to the request * if a valid SSO entry is associated with the request. It will not notify * the security <code>Realm</code> of the incoming request. * <p> * This property should be set to <code>true</code> if the overall server * configuration requires that the <code>Realm</code> reauthenticate each * request thread. An example of such a configuration would be one where * the <code>Realm</code> implementation provides security for both a * web tier and an associated EJB tier, and needs to set security * credentials on each request thread in order to support EJB access. * <p> * If this property is set to <code>true</code>, this Valve will set flags * on the request notifying the downstream Authenticator that the request * is associated with an SSO session. The Authenticator will then call its * {@link AuthenticatorBase#reauthenticateFromSSO reauthenticateFromSSO} * method to attempt to reauthenticate the request to the * <code>Realm</code>, using any credentials that were cached with this * Valve. * <p> * The default value of this property is <code>false</code>, in order * to maintain backward compatibility with previous versions of Tomcat. * * @param required <code>true</code> if it is required that a downstream * Authenticator reauthenticate each request before calls * to <code>HttpServletRequest.setUserPrincipal()</code> * and <code>HttpServletRequest.setAuthType()</code> are * made; <code>false</code> if the <code>Valve</code> can * itself make those calls relying on the presence of a * valid SingleSignOn entry associated with the request. * * @see AuthenticatorBase#reauthenticateFromSSO */
public void setRequireReauthentication(boolean required) { this.requireReauthentication = required; } // ---------------------------------------------------------- Valve Methods
Perform single-sign-on support processing for this request.
Params:
  • request – The servlet request we are processing
  • response – The servlet response we are creating
Throws:
/** * Perform single-sign-on support processing for this request. * * @param request The servlet request we are processing * @param response The servlet response we are creating * * @exception IOException if an input/output error occurs * @exception ServletException if a servlet error occurs */
@Override public void invoke(Request request, Response response) throws IOException, ServletException { request.removeNote(Constants.REQ_SSOID_NOTE); // Has a valid user already been authenticated? if (containerLog.isDebugEnabled()) { containerLog.debug(sm.getString("singleSignOn.debug.invoke", request.getRequestURI())); } if (request.getUserPrincipal() != null) { if (containerLog.isDebugEnabled()) { containerLog.debug(sm.getString("singleSignOn.debug.hasPrincipal", request.getUserPrincipal().getName())); } getNext().invoke(request, response); return; } // Check for the single sign on cookie if (containerLog.isDebugEnabled()) { containerLog.debug(sm.getString("singleSignOn.debug.cookieCheck")); } Cookie cookie = null; Cookie cookies[] = request.getCookies(); if (cookies != null) { for (Cookie value : cookies) { if (cookieName.equals(value.getName())) { cookie = value; break; } } } if (cookie == null) { if (containerLog.isDebugEnabled()) { containerLog.debug(sm.getString("singleSignOn.debug.cookieNotFound")); } getNext().invoke(request, response); return; } // Look up the cached Principal associated with this cookie value if (containerLog.isDebugEnabled()) { containerLog.debug(sm.getString("singleSignOn.debug.principalCheck", cookie.getValue())); } SingleSignOnEntry entry = cache.get(cookie.getValue()); if (entry != null) { if (containerLog.isDebugEnabled()) { containerLog.debug(sm.getString("singleSignOn.debug.principalFound", entry.getPrincipal() != null ? entry.getPrincipal().getName() : "", entry.getAuthType())); } request.setNote(Constants.REQ_SSOID_NOTE, cookie.getValue()); // Only set security elements if reauthentication is not required if (!getRequireReauthentication()) { request.setAuthType(entry.getAuthType()); request.setUserPrincipal(entry.getPrincipal()); } } else { if (containerLog.isDebugEnabled()) { containerLog.debug(sm.getString("singleSignOn.debug.principalNotFound", cookie.getValue())); } // No need to return a valid SSO session ID cookie.setValue("REMOVE"); // Age of zero will trigger removal cookie.setMaxAge(0); // Domain and path have to match the original cookie to 'replace' // the original cookie cookie.setPath("/"); String domain = getCookieDomain(); if (domain != null) { cookie.setDomain(domain); } // This is going to trigger a Set-Cookie header. While the value is // not security sensitive, ensure that expectations for secure and // httpOnly are met cookie.setSecure(request.isSecure()); if (request.getServletContext().getSessionCookieConfig().isHttpOnly() || request.getContext().getUseHttpOnly()) { cookie.setHttpOnly(true); } response.addCookie(cookie); } // Invoke the next Valve in our pipeline getNext().invoke(request, response); } // ------------------------------------------------------ Protected Methods
Process a session destroyed event by removing references to that session from the caches and - if the session destruction is the result of a logout - destroy the associated SSO session.
Params:
  • ssoId – The ID of the SSO session which which the destroyed session was associated
  • session – The session that has been destroyed
/** * Process a session destroyed event by removing references to that session * from the caches and - if the session destruction is the result of a * logout - destroy the associated SSO session. * * @param ssoId The ID of the SSO session which which the destroyed * session was associated * @param session The session that has been destroyed */
public void sessionDestroyed(String ssoId, Session session) { if (!getState().isAvailable()) { return; } // Was the session destroyed as the result of a timeout or context stop? // If so, we'll just remove the expired session from the SSO. If the // session was logged out, we'll log out of all session associated with // the SSO. if (((session.getMaxInactiveInterval() > 0) && (session.getIdleTimeInternal() >= session.getMaxInactiveInterval() * 1000)) || (!session.getManager().getContext().getState().isAvailable())) { if (containerLog.isDebugEnabled()) { containerLog.debug(sm.getString("singleSignOn.debug.sessionTimeout", ssoId, session)); } removeSession(ssoId, session); } else { // The session was logged out. // Deregister this single session id, invalidating // associated sessions if (containerLog.isDebugEnabled()) { containerLog.debug(sm.getString("singleSignOn.debug.sessionLogout", ssoId, session)); } // First remove the session that we know has expired / been logged // out since it has already been removed from its Manager and, if // we don't remove it first, deregister() will log a warning that it // can't be found removeSession(ssoId, session); // If the SSO session was only associated with one web app the call // above will have removed the SSO session from the cache if (cache.containsKey(ssoId)) { deregister(ssoId); } } }
Associate the specified single sign on identifier with the specified Session.
Params:
  • ssoId – Single sign on identifier
  • session – Session to be associated
Returns:true if the session was associated to the given SSO session, otherwise false
/** * Associate the specified single sign on identifier with the * specified Session. * * @param ssoId Single sign on identifier * @param session Session to be associated * * @return <code>true</code> if the session was associated to the given SSO * session, otherwise <code>false</code> */
protected boolean associate(String ssoId, Session session) { SingleSignOnEntry sso = cache.get(ssoId); if (sso == null) { if (containerLog.isDebugEnabled()) { containerLog.debug(sm.getString("singleSignOn.debug.associateFail", ssoId, session)); } return false; } else { if (containerLog.isDebugEnabled()) { containerLog.debug(sm.getString("singleSignOn.debug.associate", ssoId, session)); } sso.addSession(this, ssoId, session); return true; } }
Deregister the specified single sign on identifier, and invalidate any associated sessions.
Params:
  • ssoId – Single sign on identifier to deregister
/** * 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) { // Look up and remove the corresponding SingleSignOnEntry SingleSignOnEntry sso = cache.remove(ssoId); if (sso == null) { if (containerLog.isDebugEnabled()) { containerLog.debug(sm.getString("singleSignOn.debug.deregisterFail", ssoId)); } return; } // Expire any associated sessions Set<SingleSignOnSessionKey> ssoKeys = sso.findSessions(); if (ssoKeys.size() == 0) { if (containerLog.isDebugEnabled()) { containerLog.debug(sm.getString("singleSignOn.debug.deregisterNone", ssoId)); } } for (SingleSignOnSessionKey ssoKey : ssoKeys) { if (containerLog.isDebugEnabled()) { containerLog.debug(sm.getString("singleSignOn.debug.deregister", ssoKey, ssoId)); } // Invalidate this session expire(ssoKey); } // 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 } private void expire(SingleSignOnSessionKey key) { if (engine == null) { containerLog.warn(sm.getString("singleSignOn.sessionExpire.engineNull", key)); return; } Container host = engine.findChild(key.getHostName()); if (host == null) { containerLog.warn(sm.getString("singleSignOn.sessionExpire.hostNotFound", key)); return; } Context context = (Context) host.findChild(key.getContextName()); if (context == null) { containerLog.warn(sm.getString("singleSignOn.sessionExpire.contextNotFound", key)); return; } Manager manager = context.getManager(); if (manager == null) { containerLog.warn(sm.getString("singleSignOn.sessionExpire.managerNotFound", key)); return; } Session session = null; try { session = manager.findSession(key.getSessionId()); } catch (IOException e) { containerLog.warn(sm.getString("singleSignOn.sessionExpire.managerError", key), e); return; } if (session == null) { containerLog.warn(sm.getString("singleSignOn.sessionExpire.sessionNotFound", key)); return; } session.expire(); }
Attempts reauthentication to the given Realm using the credentials associated with the single sign-on session identified by argument ssoId.

If reauthentication is successful, the Principal and authorization type associated with the SSO session will be bound to the given Request object via calls to Request.setAuthType() and Request.setUserPrincipal()

Params:
  • ssoId – identifier of SingleSignOn session with which the caller is associated
  • realm – Realm implementation against which the caller is to be authenticated
  • request – the request that needs to be authenticated
Returns: true if reauthentication was successful, false otherwise.
/** * 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>Request</code> object via calls to * {@link Request#setAuthType Request.setAuthType()} and * {@link Request#setUserPrincipal Request.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 = cache.get(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.
Params:
  • ssoId – Single sign on identifier to register
  • principal – Associated user principal that is identified
  • authType – Authentication type used to authenticate this user principal
  • username – Username used to authenticate this user
  • password – Password used to authenticate this user
/** * Register the specified Principal as being associated with the specified * value for the single sign on identifier. * * @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) { if (containerLog.isDebugEnabled()) { containerLog.debug(sm.getString("singleSignOn.debug.register", ssoId, principal != null ? principal.getName() : "", authType)); } cache.put(ssoId, new SingleSignOnEntry(principal, authType, username, password)); }
Updates any SingleSignOnEntry found under key ssoId with the given authentication data.

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.

NOTE: Only updates the SSO entry if a call to SingleSignOnEntry.getCanReauthenticate() returns false; otherwise, it is assumed that the SSO entry already has sufficient information to allow reauthentication and that no update is needed.

Params:
  • ssoId – identifier of Single sign to be updated
  • principal – the Principal returned by the latest call to Realm.authenticate.
  • authType – the type of authenticator used (BASIC, CLIENT_CERT, DIGEST or FORM)
  • username – the username (if any) used for the authentication
  • password – the password (if any) used for the authentication
Returns:true if the credentials were updated, otherwise false
/** * 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. * * @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 credentials were updated, otherwise * <code>false</code> */
protected boolean update(String ssoId, Principal principal, String authType, String username, String password) { SingleSignOnEntry sso = cache.get(ssoId); if (sso != null && !sso.getCanReauthenticate()) { if (containerLog.isDebugEnabled()) { containerLog.debug(sm.getString("singleSignOn.debug.update", ssoId, authType)); } sso.updateCredentials(principal, authType, username, password); return true; } return false; }
Remove a single Session from a SingleSignOn. Called when a session is timed out and no longer active.
Params:
  • ssoId – Single sign on identifier from which to remove the session.
  • session – the session to be removed.
/** * Remove a single Session from a SingleSignOn. Called when * a session is timed out and no longer active. * * @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 (containerLog.isDebugEnabled()) { containerLog.debug(sm.getString("singleSignOn.debug.removeSession", session, ssoId)); } // Get a reference to the SingleSignOn SingleSignOnEntry entry = cache.get(ssoId); if (entry == null) { return; } // Remove the inactive session from SingleSignOnEntry entry.removeSession(session); // If there are not sessions left in the SingleSignOnEntry, // deregister the entry. if (entry.findSessions().size() == 0) { deregister(ssoId); } } protected SessionListener getSessionListener(String ssoId) { return new SingleSignOnListener(ssoId); } @Override protected synchronized void startInternal() throws LifecycleException { Container c = getContainer(); while (c != null && !(c instanceof Engine)) { c = c.getParent(); } if (c != null) { engine = (Engine) c; } super.startInternal(); } @Override protected synchronized void stopInternal() throws LifecycleException { super.stopInternal(); engine = null; } }