AuthenticatorBase.java
/*
* 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.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import jakarta.security.auth.message.AuthException;
import jakarta.security.auth.message.AuthStatus;
import jakarta.security.auth.message.MessageInfo;
import jakarta.security.auth.message.config.AuthConfigFactory;
import jakarta.security.auth.message.config.AuthConfigProvider;
import jakarta.security.auth.message.config.RegistrationListener;
import jakarta.security.auth.message.config.ServerAuthConfig;
import jakarta.security.auth.message.config.ServerAuthContext;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.catalina.Authenticator;
import org.apache.catalina.Contained;
import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Globals;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.Realm;
import org.apache.catalina.Session;
import org.apache.catalina.TomcatPrincipal;
import org.apache.catalina.Valve;
import org.apache.catalina.authenticator.jaspic.MessageInfoImpl;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.filters.CorsFilter;
import org.apache.catalina.filters.RemoteIpFilter;
import org.apache.catalina.realm.GenericPrincipal;
import org.apache.catalina.util.FilterUtil;
import org.apache.catalina.util.SessionIdGeneratorBase;
import org.apache.catalina.util.StandardSessionIdGenerator;
import org.apache.catalina.valves.RemoteIpValve;
import org.apache.catalina.valves.ValveBase;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import org.apache.tomcat.util.descriptor.web.LoginConfig;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
import org.apache.tomcat.util.http.FastHttpDateFormat;
import org.apache.tomcat.util.http.RequestUtil;
import org.apache.tomcat.util.res.StringManager;
/**
* Basic implementation of the <b>Valve</b> interface that enforces the <code><security-constraint></code>
* elements in the web application deployment descriptor. This functionality is implemented as a Valve so that it can be
* omitted in environments that do not require these features. Individual implementations of each supported
* authentication method can subclass this base class as required.
* <p>
* <b>USAGE CONSTRAINT</b>: When this class is utilized, the Context to which it is attached (or a parent Container in a
* hierarchy) must have an associated Realm that can be used for authenticating users and enumerating the roles to which
* they have been assigned.
* <p>
* <b>USAGE CONSTRAINT</b>: This Valve is only useful when processing HTTP requests. Requests of any other type will
* simply be passed through.
*
* @author Craig R. McClanahan
*/
public abstract class AuthenticatorBase extends ValveBase implements Authenticator, RegistrationListener {
private final Log log = LogFactory.getLog(AuthenticatorBase.class); // must not be static
/**
* "Expires" header always set to Date(1), so generate once only
*/
private static final String DATE_ONE = FastHttpDateFormat.formatDate(1);
/**
* The string manager for this package.
*/
protected static final StringManager sm = StringManager.getManager(AuthenticatorBase.class);
/**
* Authentication header
*/
protected static final String AUTH_HEADER_NAME = "WWW-Authenticate";
/**
* Default authentication realm name.
*/
protected static final String REALM_NAME = "Authentication required";
protected static String getRealmName(Context context) {
if (context == null) {
// Very unlikely
return REALM_NAME;
}
LoginConfig config = context.getLoginConfig();
if (config == null) {
return REALM_NAME;
}
String result = config.getRealmName();
if (result == null) {
return REALM_NAME;
}
return result;
}
// ------------------------------------------------------ Constructor
public AuthenticatorBase() {
super(true);
}
// ----------------------------------------------------- Instance Variables
/**
* Should a session always be used once a user is authenticated? This may offer some performance benefits since the
* session can then be used to cache the authenticated Principal, hence removing the need to authenticate the user
* via the Realm on every request. This may be of help for combinations such as BASIC authentication used with the
* JNDIRealm or DataSourceRealms. However there will also be the performance cost of creating and GC'ing the
* session. By default, a session will not be created.
*/
protected boolean alwaysUseSession = false;
/**
* Should we cache authenticated Principals if the request is part of an HTTP session?
*/
protected boolean cache = true;
/**
* Should the session ID, if any, be changed upon a successful authentication to prevent a session fixation attack?
*/
protected boolean changeSessionIdOnAuthentication = true;
/**
* The Context to which this Valve is attached.
*/
protected Context context = null;
/**
* Flag to determine if we disable proxy caching, or leave the issue up to the webapp developer.
*/
protected boolean disableProxyCaching = true;
/**
* Flag to determine if we disable proxy caching with headers incompatible with IE.
*/
protected boolean securePagesWithPragma = false;
/**
* The Java class name of the secure random number generator class to be used when generating SSO session
* identifiers. The random number generator class must be self-seeding and have a zero-argument constructor. If not
* specified, an instance of {@link java.security.SecureRandom} will be generated.
*/
protected String secureRandomClass = null;
/**
* The name of the algorithm to use to create instances of {@link java.security.SecureRandom} which are used to
* generate SSO session IDs. If no algorithm is specified, SHA1PRNG is used. If SHA1PRNG is not available, the
* platform default will be used. To use the platform default (which may be SHA1PRNG), specify the empty string. If
* an invalid algorithm and/or provider is specified the SecureRandom instances will be created using the defaults.
* If that fails, the SecureRandom instances will be created using platform defaults.
*/
protected String secureRandomAlgorithm = SessionIdGeneratorBase.DEFAULT_SECURE_RANDOM_ALGORITHM;
/**
* The name of the provider to use to create instances of {@link java.security.SecureRandom} which are used to
* generate session SSO IDs. If no provider is specified the platform default is used. If an invalid algorithm
* and/or provider is specified the SecureRandom instances will be created using the defaults. If that fails, the
* SecureRandom instances will be created using platform defaults.
*/
protected String secureRandomProvider = null;
/**
* The name of the JASPIC callback handler class. If none is specified the default
* {@link org.apache.catalina.authenticator.jaspic.CallbackHandlerImpl} will be used.
*/
protected String jaspicCallbackHandlerClass = "org.apache.catalina.authenticator.jaspic.CallbackHandlerImpl";
/**
* Should the auth information (remote user and auth type) be returned as response headers for a forwarded/proxied
* request? When the {@link RemoteIpValve} or {@link RemoteIpFilter} mark a forwarded request with the
* {@link Globals#REQUEST_FORWARDED_ATTRIBUTE} this authenticator can return the values of
* {@link HttpServletRequest#getRemoteUser()} and {@link HttpServletRequest#getAuthType()} as response headers
* {@code remote-user} and {@code auth-type} to a reverse proxy. This is useful, e.g., for access log consistency or
* other decisions to make.
*/
protected boolean sendAuthInfoResponseHeaders = false;
protected SessionIdGeneratorBase sessionIdGenerator = null;
/**
* The SingleSignOn implementation in our request processing chain, if there is one.
*/
protected SingleSignOn sso = null;
private AllowCorsPreflight allowCorsPreflight = AllowCorsPreflight.NEVER;
private volatile String jaspicAppContextID = null;
private volatile Optional<AuthConfigProvider> jaspicProvider = null;
private volatile CallbackHandler jaspicCallbackHandler = null;
// ------------------------------------------------------------- Properties
public String getAllowCorsPreflight() {
return allowCorsPreflight.name().toLowerCase(Locale.ENGLISH);
}
public void setAllowCorsPreflight(String allowCorsPreflight) {
this.allowCorsPreflight = AllowCorsPreflight.valueOf(allowCorsPreflight.trim().toUpperCase(Locale.ENGLISH));
}
public boolean getAlwaysUseSession() {
return alwaysUseSession;
}
public void setAlwaysUseSession(boolean alwaysUseSession) {
this.alwaysUseSession = alwaysUseSession;
}
/**
* Return the cache authenticated Principals flag.
*
* @return <code>true</code> if authenticated Principals will be cached, otherwise <code>false</code>
*/
public boolean getCache() {
return this.cache;
}
/**
* Set the cache authenticated Principals flag.
*
* @param cache The new cache flag
*/
public void setCache(boolean cache) {
this.cache = cache;
}
@Override
public Container getContainer() {
return this.context;
}
@Override
public void setContainer(Container container) {
if (container != null && !(container instanceof Context)) {
throw new IllegalArgumentException(sm.getString("authenticator.notContext"));
}
super.setContainer(container);
this.context = (Context) container;
}
/**
* Return the flag that states if we add headers to disable caching by proxies.
*
* @return <code>true</code> if the headers will be added, otherwise <code>false</code>
*/
public boolean getDisableProxyCaching() {
return disableProxyCaching;
}
/**
* Set the value of the flag that states if we add headers to disable caching by proxies.
*
* @param nocache <code>true</code> if we add headers to disable proxy caching, <code>false</code> if we leave the
* headers alone.
*/
public void setDisableProxyCaching(boolean nocache) {
disableProxyCaching = nocache;
}
/**
* Return the flag that states, if proxy caching is disabled, what headers we add to disable the caching.
*
* @return <code>true</code> if a Pragma header should be used, otherwise <code>false</code>
*/
public boolean getSecurePagesWithPragma() {
return securePagesWithPragma;
}
/**
* Set the value of the flag that states what headers we add to disable proxy caching.
*
* @param securePagesWithPragma <code>true</code> if we add headers which are incompatible with downloading office
* documents in IE under SSL but which fix a caching problem in Mozilla.
*/
public void setSecurePagesWithPragma(boolean securePagesWithPragma) {
this.securePagesWithPragma = securePagesWithPragma;
}
/**
* Return the flag that states if we should change the session ID of an existing session upon successful
* authentication.
*
* @return <code>true</code> to change session ID upon successful authentication, <code>false</code> to do not
* perform the change.
*/
public boolean getChangeSessionIdOnAuthentication() {
return changeSessionIdOnAuthentication;
}
/**
* Set the value of the flag that states if we should change the session ID of an existing session upon successful
* authentication.
*
* @param changeSessionIdOnAuthentication <code>true</code> to change session ID upon successful authentication,
* <code>false</code> to do not perform the change.
*/
public void setChangeSessionIdOnAuthentication(boolean changeSessionIdOnAuthentication) {
this.changeSessionIdOnAuthentication = changeSessionIdOnAuthentication;
}
/**
* Return the secure random number generator class name.
*
* @return The fully qualified name of the SecureRandom implementation to use
*/
public String getSecureRandomClass() {
return this.secureRandomClass;
}
/**
* Set the secure random number generator class name.
*
* @param secureRandomClass The new secure random number generator class name
*/
public void setSecureRandomClass(String secureRandomClass) {
this.secureRandomClass = secureRandomClass;
}
/**
* Return the secure random number generator algorithm name.
*
* @return The name of the SecureRandom algorithm used
*/
public String getSecureRandomAlgorithm() {
return secureRandomAlgorithm;
}
/**
* Set the secure random number generator algorithm name.
*
* @param secureRandomAlgorithm The new secure random number generator algorithm name
*/
public void setSecureRandomAlgorithm(String secureRandomAlgorithm) {
this.secureRandomAlgorithm = secureRandomAlgorithm;
}
/**
* Return the secure random number generator provider name.
*
* @return The name of the SecureRandom provider
*/
public String getSecureRandomProvider() {
return secureRandomProvider;
}
/**
* Set the secure random number generator provider name.
*
* @param secureRandomProvider The new secure random number generator provider name
*/
public void setSecureRandomProvider(String secureRandomProvider) {
this.secureRandomProvider = secureRandomProvider;
}
/**
* Return the JASPIC callback handler class name
*
* @return The name of the JASPIC callback handler
*/
public String getJaspicCallbackHandlerClass() {
return jaspicCallbackHandlerClass;
}
/**
* Set the JASPIC callback handler class name
*
* @param jaspicCallbackHandlerClass The new JASPIC callback handler class name
*/
public void setJaspicCallbackHandlerClass(String jaspicCallbackHandlerClass) {
this.jaspicCallbackHandlerClass = jaspicCallbackHandlerClass;
}
/**
* Returns the flag whether authentication information will be sent to a reverse proxy on a forwarded request.
*
* @return {@code true} if response headers shall be sent, {@code false} otherwise
*/
public boolean isSendAuthInfoResponseHeaders() {
return sendAuthInfoResponseHeaders;
}
/**
* Sets the flag whether authentication information will be send to a reverse proxy on a forwarded request.
*
* @param sendAuthInfoResponseHeaders {@code true} if response headers shall be sent, {@code false} otherwise
*/
public void setSendAuthInfoResponseHeaders(boolean sendAuthInfoResponseHeaders) {
this.sendAuthInfoResponseHeaders = sendAuthInfoResponseHeaders;
}
// --------------------------------------------------------- Public Methods
/**
* Enforce the security restrictions in the web application deployment descriptor of our associated Context.
*
* @param request Request to be processed
* @param response Response to be processed
*
* @exception IOException if an input/output error occurs
* @exception ServletException if thrown by a processing element
*/
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
if (log.isTraceEnabled()) {
log.trace("Security checking request " + request.getMethod() + " " + request.getRequestURI());
}
// Have we got a cached authenticated Principal to record?
if (cache) {
Principal principal = request.getUserPrincipal();
if (principal == null) {
Session session = request.getSessionInternal(false);
if (session != null) {
principal = session.getPrincipal();
if (principal != null) {
if (log.isTraceEnabled()) {
log.trace("We have cached auth type " + session.getAuthType() + " for principal " +
principal);
}
request.setAuthType(session.getAuthType());
request.setUserPrincipal(principal);
}
}
}
}
boolean authRequired = isContinuationRequired(request);
Realm realm = this.context.getRealm();
// Is this request URI subject to a security constraint?
SecurityConstraint[] constraints = realm.findSecurityConstraints(request, this.context);
AuthConfigProvider jaspicProvider = getJaspicProvider();
if (jaspicProvider != null) {
authRequired = true;
}
if (constraints == null && !context.getPreemptiveAuthentication() && !authRequired) {
if (log.isTraceEnabled()) {
log.trace("Not subject to any constraint");
}
getNext().invoke(request, response);
return;
}
// Make sure that constrained resources are not cached by web proxies
// or browsers as caching can provide a security hole
if (constraints != null && disableProxyCaching && !"POST".equalsIgnoreCase(request.getMethod())) {
if (securePagesWithPragma) {
// Note: These can cause problems with downloading files with IE
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", DATE_ONE);
} else {
response.setHeader("Cache-Control", "private");
}
}
if (constraints != null) {
// Enforce any user data constraint for this security constraint
if (log.isTraceEnabled()) {
log.trace("Calling hasUserDataPermission()");
}
if (!realm.hasUserDataPermission(request, response, constraints)) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("authenticator.userDataPermissionFail"));
}
/*
* ASSERT: Authenticator already set the appropriate HTTP status code, so we do not have to do anything
* special
*/
return;
}
}
// Since authenticate modifies the response on failure,
// we have to check for allow-from-all first.
boolean hasAuthConstraint = false;
if (constraints != null) {
hasAuthConstraint = true;
for (int i = 0; i < constraints.length && hasAuthConstraint; i++) {
if (!constraints[i].getAuthConstraint()) {
hasAuthConstraint = false;
} else if (!constraints[i].getAllRoles() && !constraints[i].getAuthenticatedUsers()) {
String[] roles = constraints[i].findAuthRoles();
if (roles == null || roles.length == 0) {
hasAuthConstraint = false;
}
}
}
}
if (!authRequired && hasAuthConstraint) {
authRequired = true;
}
if (!authRequired && context.getPreemptiveAuthentication() && isPreemptiveAuthPossible(request)) {
authRequired = true;
}
JaspicState jaspicState = null;
if ((authRequired || constraints != null) && allowCorsPreflightBypass(request)) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("authenticator.corsBypass"));
}
getNext().invoke(request, response);
return;
}
if (authRequired) {
if (log.isTraceEnabled()) {
log.trace("Calling authenticate()");
}
if (jaspicProvider != null) {
jaspicState = getJaspicState(jaspicProvider, request, response, hasAuthConstraint);
if (jaspicState == null) {
return;
}
}
if (jaspicProvider == null && !doAuthenticate(request, response) ||
jaspicProvider != null && !authenticateJaspic(request, response, jaspicState, false)) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("authenticator.authenticationFail"));
}
/*
* ASSERT: Authenticator already set the appropriate HTTP status code, so we do not have to do anything
* special
*/
return;
}
}
if (constraints != null) {
if (log.isTraceEnabled()) {
log.trace("Calling accessControl()");
}
if (!realm.hasResourcePermission(request, response, constraints, this.context)) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("authenticator.userPermissionFail", request.getUserPrincipal().getName()));
}
/*
* ASSERT: AccessControl method has already set the appropriate HTTP status code, so we do not have to
* do anything special
*/
return;
}
}
// Any and all specified constraints have been satisfied
if (log.isTraceEnabled()) {
log.trace("Successfully passed all security constraints");
}
getNext().invoke(request, response);
if (jaspicProvider != null) {
secureResponseJspic(request, response, jaspicState);
}
}
protected boolean allowCorsPreflightBypass(Request request) {
boolean allowBypass = false;
if (allowCorsPreflight != AllowCorsPreflight.NEVER) {
// First check to see if this is a CORS Preflight request
// This is a subset of the tests in CorsFilter.checkRequestType
if ("OPTIONS".equals(request.getMethod())) {
String originHeader = request.getHeader(CorsFilter.REQUEST_HEADER_ORIGIN);
if (originHeader != null && !originHeader.isEmpty() && RequestUtil.isValidOrigin(originHeader) &&
!RequestUtil.isSameOrigin(request, originHeader)) {
String accessControlRequestMethodHeader =
request.getHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD);
if (accessControlRequestMethodHeader != null && !accessControlRequestMethodHeader.isEmpty()) {
// This appears to be a CORS Preflight request
if (allowCorsPreflight == AllowCorsPreflight.ALWAYS) {
allowBypass = true;
} else if (allowCorsPreflight == AllowCorsPreflight.FILTER) {
if (DispatcherType.REQUEST == request.getDispatcherType()) {
// Look at Filter configuration for the Context
// Can't cache this unless we add a listener to
// the Context to clear the cache on reload
for (FilterDef filterDef : request.getContext().findFilterDefs()) {
if (CorsFilter.class.getName().equals(filterDef.getFilterClass())) {
for (FilterMap filterMap : context.findFilterMaps()) {
if (filterMap.getFilterName().equals(filterDef.getFilterName())) {
if ((filterMap.getDispatcherMapping() & FilterMap.REQUEST) > 0) {
String requestPath = FilterUtil.getRequestPath(request);
if (FilterUtil.matchFiltersURL(filterMap, requestPath)) {
allowBypass = true;
}
}
// Found mappings for CORS filter.
// No need to look further
break;
}
}
// Found the CORS filter. No need to look further.
break;
}
}
}
} else {
// Unexpected enum type
}
}
}
}
}
return allowBypass;
}
@Override
public boolean authenticate(Request request, HttpServletResponse httpResponse) throws IOException {
AuthConfigProvider jaspicProvider = getJaspicProvider();
if (jaspicProvider == null) {
return doAuthenticate(request, httpResponse);
} else {
Response response = request.getResponse();
JaspicState jaspicState = getJaspicState(jaspicProvider, request, response, true);
if (jaspicState == null) {
return false;
}
boolean result = authenticateJaspic(request, response, jaspicState, true);
secureResponseJspic(request, response, jaspicState);
return result;
}
}
private void secureResponseJspic(Request request, Response response, JaspicState state) {
try {
state.serverAuthContext.secureResponse(state.messageInfo, null);
request.setRequest((HttpServletRequest) state.messageInfo.getRequestMessage());
response.setResponse((HttpServletResponse) state.messageInfo.getResponseMessage());
} catch (AuthException e) {
log.warn(sm.getString("authenticator.jaspicSecureResponseFail"), e);
}
}
private JaspicState getJaspicState(AuthConfigProvider jaspicProvider, Request request, Response response,
boolean authMandatory) throws IOException {
JaspicState jaspicState = new JaspicState();
jaspicState.messageInfo = new MessageInfoImpl(request.getRequest(), response.getResponse(), authMandatory);
try {
CallbackHandler callbackHandler = getCallbackHandler();
ServerAuthConfig serverAuthConfig =
jaspicProvider.getServerAuthConfig("HttpServlet", jaspicAppContextID, callbackHandler);
String authContextID = serverAuthConfig.getAuthContextID(jaspicState.messageInfo);
jaspicState.serverAuthContext = serverAuthConfig.getAuthContext(authContextID, null, null);
} catch (AuthException e) {
log.warn(sm.getString("authenticator.jaspicServerAuthContextFail"), e);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return null;
}
return jaspicState;
}
private CallbackHandler getCallbackHandler() {
CallbackHandler handler = jaspicCallbackHandler;
if (handler == null) {
handler = createCallbackHandler();
}
return handler;
}
private CallbackHandler createCallbackHandler() {
CallbackHandler callbackHandler = null;
Class<?> clazz = null;
try {
clazz = Class.forName(jaspicCallbackHandlerClass, true, Thread.currentThread().getContextClassLoader());
} catch (ClassNotFoundException e) {
// Proceed with the retry below
}
try {
if (clazz == null) {
clazz = Class.forName(jaspicCallbackHandlerClass);
}
callbackHandler = (CallbackHandler) clazz.getConstructor().newInstance();
} catch (ReflectiveOperationException e) {
throw new SecurityException(e);
}
if (callbackHandler instanceof Contained) {
((Contained) callbackHandler).setContainer(getContainer());
}
jaspicCallbackHandler = callbackHandler;
return callbackHandler;
}
// ------------------------------------------------------ Protected Methods
/**
* Provided for sub-classes to implement their specific authentication mechanism.
*
* @param request The request that triggered the authentication
* @param response The response associated with the request
*
* @return {@code true} if the the user was authenticated, otherwise {@code
* false}, in which case an authentication challenge will have been written to the response
*
* @throws IOException If an I/O problem occurred during the authentication process
*/
protected abstract boolean doAuthenticate(Request request, HttpServletResponse response) throws IOException;
/**
* Does this authenticator require that {@link #authenticate(Request, HttpServletResponse)} is called to continue an
* authentication process that started in a previous request?
*
* @param request The request currently being processed
*
* @return {@code true} if authenticate() must be called, otherwise {@code false}
*/
protected boolean isContinuationRequired(Request request) {
return false;
}
/**
* Associate the specified single sign on identifier with the specified Session.
*
* @param ssoId Single sign on identifier
* @param session Session to be associated
*/
protected void associate(String ssoId, Session session) {
if (sso == null) {
return;
}
sso.associate(ssoId, session);
}
private boolean authenticateJaspic(Request request, Response response, JaspicState state,
boolean requirePrincipal) {
boolean cachedAuth = checkForCachedAuthentication(request, response, false);
Subject client = new Subject();
AuthStatus authStatus;
try {
authStatus = state.serverAuthContext.validateRequest(state.messageInfo, client, null);
} catch (AuthException e) {
log.debug(sm.getString("authenticator.loginFail"), e);
// Need to explicitly set the return code as the ServerAuthContext may not have done.
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return false;
}
request.setRequest((HttpServletRequest) state.messageInfo.getRequestMessage());
response.setResponse((HttpServletResponse) state.messageInfo.getResponseMessage());
if (authStatus == AuthStatus.SUCCESS) {
GenericPrincipal principal = getPrincipal(client);
if (log.isTraceEnabled()) {
log.trace("Authenticated user: " + principal);
}
if (principal == null) {
request.setUserPrincipal(null);
request.setAuthType(null);
if (requirePrincipal) {
return false;
}
} else if (cachedAuth == false || !principal.getUserPrincipal().equals(request.getUserPrincipal())) {
// Skip registration if authentication credentials were
// cached and the Principal did not change.
// Check to see if any of the JASPIC properties were set
Boolean register = null;
String authType = "JASPIC";
@SuppressWarnings("rawtypes") // JASPIC API uses raw types
Map map = state.messageInfo.getMap();
String registerValue = (String) map.get("jakarta.servlet.http.registerSession");
if (registerValue != null) {
register = Boolean.valueOf(registerValue);
}
String authTypeValue = (String) map.get("jakarta.servlet.http.authType");
if (authTypeValue != null) {
authType = authTypeValue;
}
/*
* Need to handle three cases. See https://bz.apache.org/bugzilla/show_bug.cgi?id=64713 1.
* registerSession TRUE always use session, always cache 2. registerSession NOT SET config for session,
* config for cache 3. registerSession FALSE config for session, never cache
*/
if (register != null) {
register(request, response, principal, authType, null, null,
alwaysUseSession || register.booleanValue(), register.booleanValue());
} else {
register(request, response, principal, authType, null, null);
}
}
request.setNote(Constants.REQ_JASPIC_SUBJECT_NOTE, client);
return true;
}
return false;
}
private GenericPrincipal getPrincipal(Subject subject) {
if (subject == null) {
return null;
}
Set<GenericPrincipal> principals = subject.getPrivateCredentials(GenericPrincipal.class);
if (principals.isEmpty()) {
return null;
}
return principals.iterator().next();
}
/**
* Check to see if the user has already been authenticated earlier in the processing chain or if there is enough
* information available to authenticate the user without requiring further user interaction.
*
* @param request The current request
* @param response The current response
* @param useSSO Should information available from SSO be used to attempt to authenticate the current user?
*
* @return <code>true</code> if the user was authenticated via the cache, otherwise <code>false</code>
*/
protected boolean checkForCachedAuthentication(Request request, HttpServletResponse response, boolean useSSO) {
// Has the user already been authenticated?
Principal principal = request.getUserPrincipal();
String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE);
if (principal != null) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("authenticator.check.found", principal.getName()));
}
// Associate the session with any existing SSO session. Even if
// useSSO is false, this will ensure coordinated session
// invalidation at log out.
if (ssoId != null) {
associate(ssoId, request.getSessionInternal(true));
}
return true;
}
// Is there an SSO session against which we can try to reauthenticate?
if (useSSO && ssoId != null) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("authenticator.check.sso", ssoId));
}
/*
* Try to reauthenticate using data cached by SSO. If this fails, either the original SSO logon was of
* DIGEST or SSL (which we can't reauthenticate ourselves because there is no cached username and password),
* or the realm denied the user's reauthentication for some reason. In either case we have to prompt the
* user for a logon
*/
if (reauthenticateFromSSO(ssoId, request)) {
return true;
}
}
// Has the Connector provided a pre-authenticated Principal that now
// needs to be authorized?
if (request.getCoyoteRequest().getRemoteUserNeedsAuthorization()) {
String username = request.getCoyoteRequest().getRemoteUser().toString();
if (username != null) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("authenticator.check.authorize", username));
}
Principal authorized = context.getRealm().authenticate(username);
if (authorized == null) {
// Realm doesn't recognise user. Create a user with no roles
// from the authenticated user name
if (log.isDebugEnabled()) {
log.debug(sm.getString("authenticator.check.authorizeFail", username));
}
authorized = new GenericPrincipal(username);
}
String authType = request.getAuthType();
if (authType == null || authType.length() == 0) {
authType = getAuthMethod();
}
register(request, response, authorized, authType, username, null);
return true;
}
}
return false;
}
/**
* Attempts reauthentication to the <code>Realm</code> using the credentials included in argument
* <code>entry</code>.
*
* @param ssoId identifier of SingleSignOn session with which the caller is associated
* @param request the request that needs to be authenticated
*
* @return <code>true</code> if the reauthentication from SSL occurred
*/
protected boolean reauthenticateFromSSO(String ssoId, Request request) {
if (sso == null || ssoId == null) {
return false;
}
boolean reauthenticated = false;
Container parent = getContainer();
if (parent != null) {
Realm realm = parent.getRealm();
if (realm != null) {
reauthenticated = sso.reauthenticate(ssoId, realm, request);
}
}
if (reauthenticated) {
associate(ssoId, request.getSessionInternal(true));
if (log.isDebugEnabled()) {
log.debug(sm.getString("authenticator.reauthentication", request.getUserPrincipal().getName(),
request.getAuthType()));
}
}
return reauthenticated;
}
/**
* Register an authenticated Principal and authentication type in our request, in the current session (if there is
* one), and with our SingleSignOn valve, if there is one. Set the appropriate cookie to be returned.
*
* @param request The servlet request we are processing
* @param response The servlet response we are generating
* @param principal The authenticated Principal to be registered
* @param authType The authentication type to be registered
* @param username Username used to authenticate (if any)
* @param password Password used to authenticate (if any)
*/
public void register(Request request, HttpServletResponse response, Principal principal, String authType,
String username, String password) {
register(request, response, principal, authType, username, password, alwaysUseSession, cache);
}
/**
* Register an authenticated Principal and authentication type in our request, in the current session (if there is
* one), and with our SingleSignOn valve, if there is one. Set the appropriate cookie to be returned.
*
* @param request The servlet request we are processing
* @param response The servlet response we are generating
* @param principal The authenticated Principal to be registered
* @param authType The authentication type to be registered
* @param username Username used to authenticate (if any)
* @param password Password used to authenticate (if any)
* @param alwaysUseSession Should a session always be used once a user is authenticated?
* @param cache Should we cache authenticated Principals if the request is part of an HTTP session?
*/
protected void register(Request request, HttpServletResponse response, Principal principal, String authType,
String username, String password, boolean alwaysUseSession, boolean cache) {
if (log.isDebugEnabled()) {
String name = (principal == null) ? "none" : principal.getName();
log.debug(sm.getString("authenticator.authentication", name, authType));
}
// Cache the authentication information in our request
request.setAuthType(authType);
request.setUserPrincipal(principal);
if (sendAuthInfoResponseHeaders &&
Boolean.TRUE.equals(request.getAttribute(Globals.REQUEST_FORWARDED_ATTRIBUTE))) {
response.setHeader("remote-user", request.getRemoteUser());
response.setHeader("auth-type", request.getAuthType());
}
Session session = request.getSessionInternal(false);
if (session != null) {
// If the principal is null then this is a logout. No need to change
// the session ID. See BZ 59043.
if (getChangeSessionIdOnAuthentication() && principal != null) {
String newSessionId = changeSessionID(request, session);
// If the current session ID is being tracked, update it.
if (session.getNote(Constants.SESSION_ID_NOTE) != null) {
session.setNote(Constants.SESSION_ID_NOTE, newSessionId);
}
}
} else if (alwaysUseSession) {
session = request.getSessionInternal(true);
}
// Cache the authentication information in our session, if any
if (session != null && cache) {
session.setAuthType(authType);
session.setPrincipal(principal);
}
// Construct a cookie to be returned to the client
if (sso == null) {
return;
}
// Only create a new SSO entry if the SSO did not already set a note
// for an existing entry (as it would do with subsequent requests
// for DIGEST and SSL authenticated contexts)
String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE);
if (ssoId == null) {
// Construct a cookie to be returned to the client
ssoId = sessionIdGenerator.generateSessionId();
Cookie cookie = new Cookie(sso.getCookieName(), ssoId);
cookie.setMaxAge(-1);
cookie.setPath("/");
// Bugzilla 41217
cookie.setSecure(request.isSecure());
// Bugzilla 34724
String ssoDomain = sso.getCookieDomain();
if (ssoDomain != null) {
cookie.setDomain(ssoDomain);
}
// Configure httpOnly on SSO cookie using same rules as session cookies
if (request.getServletContext().getSessionCookieConfig().isHttpOnly() ||
request.getContext().getUseHttpOnly()) {
cookie.setHttpOnly(true);
}
// Configure Partitioned on SSO cookie using same rules as session cookies
cookie.setAttribute(Constants.COOKIE_PARTITIONED_ATTR,
Boolean.toString(request.getContext().getUsePartitioned()));
response.addCookie(cookie);
// Register this principal with our SSO valve
sso.register(ssoId, principal, authType, username, password);
request.setNote(Constants.REQ_SSOID_NOTE, ssoId);
} else {
if (principal == null) {
// Registering a programmatic logout
sso.deregister(ssoId);
request.removeNote(Constants.REQ_SSOID_NOTE);
return;
} else {
// Update the SSO session with the latest authentication data
sso.update(ssoId, principal, authType, username, password);
}
}
// Fix for Bug 10040
// Always associate a session with a new SSO registration.
// SSO entries are only removed from the SSO registry map when
// associated sessions are destroyed; if a new SSO entry is created
// above for this request and the user never revisits the context, the
// SSO entry will never be cleared if we don't associate the session
if (session == null) {
session = request.getSessionInternal(true);
}
sso.associate(ssoId, session);
}
protected String changeSessionID(Request request, Session session) {
String oldId = null;
if (log.isDebugEnabled()) {
oldId = session.getId();
}
String newId = request.changeSessionId();
if (log.isDebugEnabled()) {
log.debug(sm.getString("authenticator.changeSessionId", oldId, newId));
}
return newId;
}
@Override
public void login(String username, String password, Request request) throws ServletException {
Principal principal = doLogin(request, username, password);
register(request, request.getResponse(), principal, getAuthMethod(), username, password);
}
/**
* Return the authentication method, which is vendor-specific and not defined by HttpServletRequest.
*
* @return the authentication method, which is vendor-specific and not defined by HttpServletRequest.
*/
protected abstract String getAuthMethod();
/**
* Process the login request.
*
* @param request Associated request
* @param username The user
* @param password The password
*
* @return The authenticated Principal
*
* @throws ServletException No principal was authenticated with the specified credentials
*/
protected Principal doLogin(Request request, String username, String password) throws ServletException {
Principal p = context.getRealm().authenticate(username, password);
if (p == null) {
throw new ServletException(sm.getString("authenticator.loginFail"));
}
return p;
}
@Override
public void logout(Request request) {
AuthConfigProvider provider = getJaspicProvider();
if (provider != null) {
MessageInfo messageInfo = new MessageInfoImpl(request, request.getResponse(), true);
Subject client = (Subject) request.getNote(Constants.REQ_JASPIC_SUBJECT_NOTE);
if (client != null) {
ServerAuthContext serverAuthContext;
try {
ServerAuthConfig serverAuthConfig =
provider.getServerAuthConfig("HttpServlet", jaspicAppContextID, getCallbackHandler());
String authContextID = serverAuthConfig.getAuthContextID(messageInfo);
serverAuthContext = serverAuthConfig.getAuthContext(authContextID, null, null);
serverAuthContext.cleanSubject(messageInfo, client);
} catch (AuthException e) {
log.debug(sm.getString("authenticator.jaspicCleanSubjectFail"), e);
}
}
}
Principal p = request.getPrincipal();
if (p instanceof TomcatPrincipal) {
try {
((TomcatPrincipal) p).logout();
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.debug(sm.getString("authenticator.tomcatPrincipalLogoutFail"), t);
}
}
register(request, request.getResponse(), null, null, null, null);
}
@Override
protected void startInternal() throws LifecycleException {
ServletContext servletContext = context.getServletContext();
jaspicAppContextID = servletContext.getVirtualServerName() + " " + servletContext.getContextPath();
// Look up the SingleSignOn implementation in our request processing
// path, if there is one
Container parent = context.getParent();
while ((sso == null) && (parent != null)) {
Valve valves[] = parent.getPipeline().getValves();
for (Valve valve : valves) {
if (valve instanceof SingleSignOn) {
sso = (SingleSignOn) valve;
break;
}
}
if (sso == null) {
parent = parent.getParent();
}
}
if (log.isDebugEnabled()) {
if (sso != null) {
log.debug(sm.getString("authenticator.sso", sso));
} else {
log.trace("No SingleSignOn Valve is present");
}
}
sessionIdGenerator = new StandardSessionIdGenerator();
sessionIdGenerator.setSecureRandomAlgorithm(getSecureRandomAlgorithm());
sessionIdGenerator.setSecureRandomClass(getSecureRandomClass());
sessionIdGenerator.setSecureRandomProvider(getSecureRandomProvider());
super.startInternal();
}
@Override
protected void stopInternal() throws LifecycleException {
super.stopInternal();
sso = null;
}
/**
* Can the authenticator perform preemptive authentication for the given request?
*
* @param request The request to check for credentials
*
* @return {@code true} if preemptive authentication is possible, otherwise {@code false}
*/
protected boolean isPreemptiveAuthPossible(Request request) {
return false;
}
private AuthConfigProvider getJaspicProvider() {
Optional<AuthConfigProvider> provider = jaspicProvider;
if (provider == null) {
provider = findJaspicProvider();
}
return provider.orElse(null);
}
private Optional<AuthConfigProvider> findJaspicProvider() {
AuthConfigFactory factory = AuthConfigFactory.getFactory();
Optional<AuthConfigProvider> provider;
if (factory == null) {
provider = Optional.empty();
} else {
provider = Optional.ofNullable(factory.getConfigProvider("HttpServlet", jaspicAppContextID, this));
}
jaspicProvider = provider;
return provider;
}
@Override
public void notify(String layer, String appContext) {
findJaspicProvider();
}
private static class JaspicState {
public MessageInfo messageInfo = null;
public ServerAuthContext serverAuthContext = null;
}
protected enum AllowCorsPreflight {
NEVER,
FILTER,
ALWAYS
}
}