org.eclipse.jetty.security.SecurityHandler Maven / Gradle / Ivy
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.security;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.pathmap.MappedResource;
import org.eclipse.jetty.http.pathmap.PathMappings;
import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.http.pathmap.PathSpecGroup;
import org.eclipse.jetty.security.Authenticator.Configuration;
import org.eclipse.jetty.security.Constraint.Authorization;
import org.eclipse.jetty.security.Constraint.Transport;
import org.eclipse.jetty.security.authentication.LoginAuthenticator;
import org.eclipse.jetty.server.Context;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.component.DumpableCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract SecurityHandler.
*
* Select and apply an {@link Authenticator} to a request.
*
* The Authenticator may either be directly set on the handler
* or it will be created during {@link #start()} with a call to
* either the default or set AuthenticatorFactory.
*
* SecurityHandler has a set of parameters that are used by the
* Authentication.Configuration. At startup, any context init parameters
* that start with "org.eclipse.jetty.security." that do not have
* values in the SecurityHandler init parameters, are copied.
*/
public abstract class SecurityHandler extends Handler.Wrapper implements Configuration
{
public static String SESSION_AUTHENTICATED_ATTRIBUTE = "org.eclipse.jetty.security.sessionAuthenticated";
private static final Logger LOG = LoggerFactory.getLogger(SecurityHandler.class);
private static final List __knownAuthenticatorFactories = new ArrayList<>();
private Authenticator _authenticator;
private Authenticator.Factory _authenticatorFactory;
private String _realmName;
private String _authenticationType;
private final Map _parameters = new HashMap<>();
private LoginService _loginService;
private IdentityService _identityService;
private boolean _renewSessionOnAuthentication = true;
private int _sessionMaxInactiveIntervalOnAuthentication = 0;
private AuthenticationState.Deferred _deferred;
static
{
TypeUtil.serviceStream(ServiceLoader.load(Authenticator.Factory.class))
.forEach(__knownAuthenticatorFactories::add);
__knownAuthenticatorFactories.add(new DefaultAuthenticatorFactory());
}
protected SecurityHandler()
{
this(null);
}
protected SecurityHandler(Handler handler)
{
super(handler);
installBean(new DumpableCollection("knownAuthenticatorFactories", __knownAuthenticatorFactories));
}
/**
* Get the identityService.
*
* @return the identityService
*/
@Override
public IdentityService getIdentityService()
{
return _identityService;
}
/**
* Set the identityService.
*
* @param identityService the identityService to set
*/
public void setIdentityService(IdentityService identityService)
{
if (isStarted())
throw new IllegalStateException("Started");
updateBean(_identityService, identityService);
_identityService = identityService;
}
/**
* Get the loginService.
*
* @return the loginService
*/
@Override
public LoginService getLoginService()
{
return _loginService;
}
/**
* Set the loginService.
* If a {@link LoginService} is not set, or is set to null,
* then during {@link #doStart()}
* the {@link #findLoginService()} method is used to locate one.
*
* @param loginService the loginService to set
*/
public void setLoginService(LoginService loginService)
{
if (isStarted())
throw new IllegalStateException("Started");
updateBean(_loginService, loginService);
_loginService = loginService;
}
public Authenticator getAuthenticator()
{
return _authenticator;
}
/**
* Set the authenticator.
*
* @param authenticator the authenticator
* @throws IllegalStateException if the SecurityHandler is running
*/
public void setAuthenticator(Authenticator authenticator)
{
if (isStarted())
throw new IllegalStateException("Started");
updateBean(_authenticator, authenticator);
_authenticator = authenticator;
if (_authenticator != null)
_authenticationType = _authenticator.getAuthenticationType();
}
/**
* @return the authenticatorFactory
*/
public Authenticator.Factory getAuthenticatorFactory()
{
return _authenticatorFactory;
}
/**
* @param authenticatorFactory the authenticatorFactory to set
* @throws IllegalStateException if the SecurityHandler is running
*/
public void setAuthenticatorFactory(Authenticator.Factory authenticatorFactory)
{
if (isRunning())
throw new IllegalStateException("running");
updateBean(_authenticatorFactory, authenticatorFactory);
_authenticatorFactory = authenticatorFactory;
}
/**
* @return the list of discovered authenticatorFactories
*/
public List getKnownAuthenticatorFactories()
{
return __knownAuthenticatorFactories;
}
/**
* @return the realmName
*/
@Override
public String getRealmName()
{
return _realmName;
}
/**
* @param realmName the realmName to set
* @throws IllegalStateException if the SecurityHandler is running
*/
public void setRealmName(String realmName)
{
if (isRunning())
throw new IllegalStateException("running");
_realmName = realmName;
}
/**
* @return the name of the Authenticator
*/
@Override
public String getAuthenticationType()
{
return _authenticationType;
}
/**
* @param authenticationType the name of the Authenticator to use
* @throws IllegalStateException if the SecurityHandler is running
*/
public void setAuthenticationType(String authenticationType)
{
if (isRunning())
throw new IllegalStateException("running");
_authenticationType = authenticationType;
if (_authenticator != null && !_authenticator.getAuthenticationType().equals(_authenticationType))
_authenticator = null;
}
@Override
public String getParameter(String key)
{
return _parameters.get(key);
}
@Override
public Set getParameterNames()
{
return _parameters.keySet();
}
/**
* Set an authentication parameter for retrieval via {@link Configuration#getParameter(String)}
*
* @param key the key
* @param value the init value
* @return previous value
* @throws IllegalStateException if the SecurityHandler is started
*/
public String setParameter(String key, String value)
{
if (isStarted())
throw new IllegalStateException("started");
return _parameters.put(key, value);
}
/**
* Find an appropriate {@link LoginService} from the
* list returned by {@link org.eclipse.jetty.util.component.Container#getBeans(Class)}
* called on the result of {@link #getServer()}. A service is selected by:
*
* - if {@link #setRealmName(String)} has been called, the first service
* with a matching name is used
* - if the list is size 1, that service is used
* - otherwise no service is selected.
*
* @return An appropriate {@link LoginService} or null
*/
protected LoginService findLoginService()
{
java.util.Collection list = getServer().getBeans(LoginService.class);
LoginService service = null;
String realm = getRealmName();
if (realm != null)
{
for (LoginService s : list)
{
if (s.getName() != null && s.getName().equals(realm))
{
service = s;
break;
}
}
}
else if (list.size() == 1)
service = list.iterator().next();
return service;
}
protected IdentityService findIdentityService()
{
return getServer().getBean(IdentityService.class);
}
@Override
protected void doStart()
throws Exception
{
// complicated resolution of login and identity service to handle
// many different ways these can be constructed and injected.
if (_loginService == null)
{
setLoginService(findLoginService());
if (_loginService != null)
unmanage(_loginService);
}
if (_identityService == null)
{
if (_loginService != null)
setIdentityService(_loginService.getIdentityService());
if (_identityService == null)
setIdentityService(findIdentityService());
if (_identityService == null)
{
setIdentityService(new DefaultIdentityService());
manage(_identityService);
}
else
unmanage(_identityService);
}
if (_loginService != null)
{
if (_loginService.getIdentityService() == null)
_loginService.setIdentityService(_identityService);
else if (_loginService.getIdentityService() != _identityService)
throw new IllegalStateException("LoginService has different IdentityService to " + this);
}
Context context = ContextHandler.getCurrentContext();
if (_authenticator == null)
{
// If someone has set an authenticator factory only use that, otherwise try the list of discovered factories.
if (_authenticatorFactory != null)
{
Authenticator authenticator = _authenticatorFactory.getAuthenticator(getServer(), context,
this);
if (authenticator != null)
{
if (LOG.isDebugEnabled())
LOG.debug("Created authenticator {} with {}", authenticator, _authenticatorFactory);
setAuthenticator(authenticator);
}
}
else
{
for (Authenticator.Factory factory : getKnownAuthenticatorFactories())
{
Authenticator authenticator = factory.getAuthenticator(getServer(), context,
this);
if (authenticator != null)
{
if (LOG.isDebugEnabled())
LOG.debug("Created authenticator {} with {}", authenticator, factory);
setAuthenticator(authenticator);
break;
}
}
}
}
if (_authenticator == null)
setAuthenticator(new Authenticator.NoOp());
if (_authenticator != null)
_authenticator.setConfiguration(this);
else if (_realmName != null)
{
LOG.warn("No Authenticator for {}", this);
throw new IllegalStateException("No Authenticator");
}
if (_authenticator instanceof LoginAuthenticator loginAuthenticator)
{
_deferred = AuthenticationState.defer(loginAuthenticator);
addBean(_deferred);
}
super.doStart();
}
@Override
protected void doStop() throws Exception
{
//if we discovered the services (rather than had them explicitly configured), remove them.
if (!isManaged(_identityService))
{
removeBean(_identityService);
_identityService = null;
}
if (!isManaged(_loginService))
{
removeBean(_loginService);
_loginService = null;
}
if (_deferred != null)
{
removeBean(_deferred);
_deferred = null;
}
super.doStop();
}
@Override
public boolean isSessionRenewedOnAuthentication()
{
return _renewSessionOnAuthentication;
}
/**
* Set renew the session on Authentication.
*
* If set to true, then on authentication, the session associated with a request is invalidated and replaced with a new session.
*
* @param renew true to renew the authentication on session
* @see Configuration#isSessionRenewedOnAuthentication()
*/
public void setSessionRenewedOnAuthentication(boolean renew)
{
_renewSessionOnAuthentication = renew;
}
@Override
public int getSessionMaxInactiveIntervalOnAuthentication()
{
return _sessionMaxInactiveIntervalOnAuthentication;
}
/**
* Set the interval in seconds, which if non-zero, will be set with
* {@link org.eclipse.jetty.server.Session#setMaxInactiveInterval(int)}
* when a session is newly authenticated.
* @param seconds An interval in seconds; or 0 to not set the interval
* on authentication; or a negative number to make the
* session never timeout after authentication.
* @see Configuration#getSessionMaxInactiveIntervalOnAuthentication()
*/
public void setSessionMaxInactiveIntervalOnAuthentication(int seconds)
{
_sessionMaxInactiveIntervalOnAuthentication = seconds;
}
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
Handler next = getHandler();
if (next == null)
return false;
// Skip security check if this is a dispatch rather than a fresh request
if (request.getContext().isCrossContextDispatch(request))
return next.handle(request, response, callback);
String pathInContext = Request.getPathInContext(request);
Constraint constraint = getConstraint(pathInContext, request);
if (LOG.isDebugEnabled())
LOG.debug("getConstraint({}) -> {}", pathInContext, constraint);
if (constraint == null)
constraint = Constraint.ALLOWED;
if (constraint.getAuthorization() == Authorization.FORBIDDEN)
{
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403);
return true;
}
// Check data constraints
if (Transport.SECURE.equals(constraint.getTransport()) && !request.isSecure())
{
redirectToSecure(request, response, callback);
return true;
}
// Determine Constraint.Authentication
Authorization constraintAuthorization = constraint.getAuthorization();
constraintAuthorization = _authenticator.getConstraintAuthentication(pathInContext, constraintAuthorization, request::getSession);
if (constraintAuthorization == Authorization.INHERIT)
constraintAuthorization = Authorization.ALLOWED;
if (LOG.isDebugEnabled())
LOG.debug("constraintAuthorization {}", constraintAuthorization);
boolean mustValidate = constraintAuthorization != Authorization.ALLOWED;
try
{
AuthenticationState authenticationState = mustValidate ? _authenticator.validateRequest(request, response, callback) : null;
if (LOG.isDebugEnabled())
LOG.debug("AuthenticationState {}", authenticationState);
if (authenticationState instanceof AuthenticationState.ResponseSent)
return true;
if (authenticationState instanceof AuthenticationState.ServeAs serveAs)
{
HttpURI uri = request.getHttpURI();
request = serveAs.wrap(request);
if (!uri.equals(request.getHttpURI()))
{
// URI is replaced, so filter out all metadata for the old URI
response.getHeaders().put(HttpHeader.CACHE_CONTROL.asString(), HttpHeaderValue.NO_CACHE.asString());
response.getHeaders().putDate(HttpHeader.EXPIRES.asString(), 1);
HttpFields.Mutable headers = new HttpFields.Mutable.Wrapper(response.getHeaders())
{
@Override
public HttpField onAddField(HttpField field)
{
if (field.getHeader() == null)
return field;
return switch (field.getHeader())
{
case CACHE_CONTROL, PRAGMA, ETAG, EXPIRES, LAST_MODIFIED, AGE -> null;
default -> field;
};
}
};
response = new Response.Wrapper(request, response)
{
@Override
public HttpFields.Mutable getHeaders()
{
return headers;
}
};
}
authenticationState = _deferred;
}
else if (mustValidate && !isAuthorized(constraint, authenticationState))
{
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "!authorized");
return true;
}
else if (authenticationState == null)
{
authenticationState = _deferred;
}
AuthenticationState.setAuthenticationState(request, authenticationState);
IdentityService.Association association =
(authenticationState instanceof AuthenticationState.Succeeded user)
? _identityService.associate(user.getUserIdentity(), null) : null;
try
{
//process the request by other handlers
return next.handle(_authenticator.prepareRequest(request, authenticationState), response, callback);
}
finally
{
if (association == null && authenticationState instanceof AuthenticationState.Deferred deferred)
association = deferred.getAssociation();
if (association != null)
association.close();
}
}
catch (ServerAuthException e)
{
Response.writeError(request, response, callback, HttpStatus.INTERNAL_SERVER_ERROR_500, e.getMessage());
return true;
}
}
public static SecurityHandler getCurrentSecurityHandler()
{
ContextHandler contextHandler = ContextHandler.getCurrentContextHandler();
if (contextHandler != null)
return contextHandler.getDescendant(SecurityHandler.class);
return null;
}
protected abstract Constraint getConstraint(String pathInContext, Request request);
protected void redirectToSecure(Request request, Response response, Callback callback)
{
HttpConfiguration httpConfig = request.getConnectionMetaData().getHttpConfiguration();
if (httpConfig.getSecurePort() > 0)
{
//Redirect to secure port
String scheme = httpConfig.getSecureScheme();
int port = httpConfig.getSecurePort();
String url = URIUtil.newURI(scheme, Request.getServerName(request), port, request.getHttpURI().getPath(), request.getHttpURI().getQuery());
response.getHeaders().put(HttpFields.CONTENT_LENGTH_0);
Response.sendRedirect(request, response, callback, HttpStatus.MOVED_TEMPORARILY_302, url, true);
}
else
{
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "!Secure");
}
}
protected boolean isAuthorized(Constraint constraint, AuthenticationState authenticationState)
{
UserIdentity userIdentity = authenticationState instanceof AuthenticationState.Succeeded user ? user.getUserIdentity() : null;
return switch (constraint.getAuthorization())
{
case FORBIDDEN, ALLOWED, INHERIT -> true;
case ANY_USER -> userIdentity != null && userIdentity.getUserPrincipal() != null;
case KNOWN_ROLE ->
{
if (userIdentity != null && userIdentity.getUserPrincipal() != null)
for (String role : getKnownRoles())
if (userIdentity.isUserInRole(role))
yield true;
yield false;
}
case SPECIFIC_ROLE ->
{
if (userIdentity != null && userIdentity.getUserPrincipal() != null)
for (String role : constraint.getRoles())
if (userIdentity.isUserInRole(role))
yield true;
yield false;
}
};
}
protected Set getKnownRoles()
{
return Collections.emptySet();
}
public class NotChecked implements Principal
{
@Override
public String getName()
{
return null;
}
@Override
public String toString()
{
return "NOT CHECKED";
}
public SecurityHandler getSecurityHandler()
{
return SecurityHandler.this;
}
}
// TODO consider method mapping version
/**
* A concrete implementation of {@link SecurityHandler} that uses a {@link PathMappings} to
* match request to a list of {@link Constraint}s, which are applied in the order of
* least significant to most significant.
*
* An example of using this class is:
*
* SecurityHandler.PathMapped handler = new SecurityHandler.PathMapped();
* handler.put("/*", Constraint.combine(Constraint.FORBIDDEN, Constraint.SECURE_TRANSPORT);
* handler.put("", Constraint.ALLOWED);
* handler.put("/login", Constraint.ALLOWED);
* handler.put("*.png", Constraint.ANY_TRANSPORT);
* handler.put("/admin/*", Constraint.from("admin", "operator"));
* handler.put("/admin/super/*", Constraint.from("operator"));
* handler.put("/user/*", Constraint.ANY_USER);
* handler.put("*.xml", Constraint.FORBIDDEN);
*
*
* When {@link #getConstraint(String, Request)} is called, any matching
* constraints are sorted into least to most significant with
* {@link #compare(PathSpec, PathSpec)}, resulting in the order in which
* {@link Constraint#combine(Constraint, Constraint)} will be applied.
* For example:
*
*
* - {@code "/admin/index.html"} matches {@code "/*"} and {@code "/admin/*"}, resulting in a
* constraint of {@link Authorization#SPECIFIC_ROLE} and {@link Transport#SECURE}.
* - {@code "/admin/logo.png"} matches {@code "/*"}, {@code "/admin/*"} and {@code "*.png"}, resulting in a
* constraint of {@link Authorization#SPECIFIC_ROLE} and {@link Transport#ANY}.
* - {@code "/admin/config.xml"} matches {@code "/*"}, {@code "/admin/*"} and {@code "*.xml"}, resulting in a
* constraint of {@link Authorization#FORBIDDEN} and {@link Transport#SECURE}.
* - {@code "/admin/super/index.html"} matches {@code "/*"}, {@code "/admin/*"} and {@code "/admin/super/*"}, resulting in a
* constraint of {@link Authorization#SPECIFIC_ROLE} and {@link Transport#SECURE}.
*
*/
public static class PathMapped extends SecurityHandler implements Comparator
{
private final PathMappings _mappings = new PathMappings<>();
private final Set _knownRoles = new HashSet<>();
public PathMapped()
{
this(null);
}
public PathMapped(Handler handler)
{
super(handler);
}
public Constraint put(String pathSpec, Constraint constraint)
{
return put(PathSpec.from(pathSpec), constraint);
}
public Constraint put(PathSpec pathSpec, Constraint constraint)
{
Set roles = constraint.getRoles();
if (roles != null)
_knownRoles.addAll(roles);
return _mappings.put(pathSpec, constraint);
}
public Constraint get(PathSpec pathSpec)
{
return _mappings.get(pathSpec);
}
public Constraint remove(PathSpec pathSpec)
{
Constraint removed = _mappings.remove(pathSpec);
_knownRoles.clear();
_mappings.values().forEach(c ->
{
Set roles = c.getRoles();
if (roles != null)
_knownRoles.addAll(roles);
});
return removed;
}
@Override
protected Constraint getConstraint(String pathInContext, Request request)
{
List> matches = _mappings.getMatches(pathInContext);
if (matches == null || matches.isEmpty())
return null;
if (matches.size() == 1)
return matches.get(0).getResource();
// apply from least specific to most specific
matches.sort(this::compare);
if (LOG.isDebugEnabled())
LOG.debug("getConstraint {} -> {}", pathInContext, matches);
Constraint constraint = null;
for (MappedResource c : matches)
constraint = Constraint.combine(constraint, c.getResource());
return constraint;
}
/**
* {@link Comparator} method to sort paths from least specific to most specific. Using
* the {@link #pathSpecGroupPrecedence(PathSpecGroup)} to rank different groups and
* {@link PathSpec#getSpecLength()} to rank within a group. This method may be overridden
* to provide different precedence between constraints.
* @param ps1 the first {@code PathSpec} to be compared.
* @param ps2 the second {@code PathSpec} to be compared.
* @return -1, 0 or 1
*/
@Override
public int compare(PathSpec ps1, PathSpec ps2)
{
PathSpecGroup g1 = ps1.getGroup();
PathSpecGroup g2 = ps2.getGroup();
if (g1.equals(g2))
return Integer.compare(ps1.getSpecLength(), ps2.getSpecLength());
return Integer.compare(pathSpecGroupPrecedence(g1), pathSpecGroupPrecedence(g2));
}
int compare(MappedResource c1, MappedResource c2)
{
PathSpecGroup g1 = c1.getPathSpec().getGroup();
PathSpecGroup g2 = c2.getPathSpec().getGroup();
int l1 = c1.getPathSpec().getSpecLength();
int l2 = c2.getPathSpec().getSpecLength();
if (g1.equals(g2))
return Integer.compare(l1, l2);
return Integer.compare(pathSpecGroupPrecedence(g1), pathSpecGroupPrecedence(g2));
}
/**
* Get the relative precedence of a {@link PathSpecGroup} used by {@link #compare(MappedResource, MappedResource)}
* to sort {@link Constraint}s. The precedence from most significant to least is:
*
* - {@link PathSpecGroup#EXACT}
* - {@link PathSpecGroup#ROOT}
* - {@link PathSpecGroup#SUFFIX_GLOB}
* - {@link PathSpecGroup#MIDDLE_GLOB}
* - {@link PathSpecGroup#PREFIX_GLOB}
* - {@link PathSpecGroup#DEFAULT}
*
* @param group The group to rank.
* @return An integer representing relative precedence between {@link PathSpecGroup}s.
*/
protected int pathSpecGroupPrecedence(PathSpecGroup group)
{
return switch (group)
{
case EXACT -> 5;
case ROOT -> 4;
case SUFFIX_GLOB -> 3;
case MIDDLE_GLOB -> 2;
case PREFIX_GLOB -> 1;
case DEFAULT -> 0;
};
}
@Override
protected Set getKnownRoles()
{
return _knownRoles;
}
}
}