Implementation of the SPNEGO (or "Negotiate") authentication defined in RFC 4559.
+ *A {@link #getUserName() user} is logged in via JAAS (either via userName/password or + * via userName/keyTab) once only.
+ *For every request that needs authentication, a {@link GSSContext} is initiated and + * later established after reading the response from the server.
+ *Applications should create objects of this class and add them to the + * {@link AuthenticationStore} retrieved from the {@link HttpClient} + * via {@link HttpClient#getAuthenticationStore()}.
+ */ +public class SPNEGOAuthentication extends AbstractAuthentication +{ + private static final Logger LOG = Log.getLogger(SPNEGOAuthentication.class); + private static final String NEGOTIATE = HttpHeader.NEGOTIATE.asString(); + + private final GSSManager gssManager = GSSManager.getInstance(); + private String userName; + private String userPassword; + private Path userKeyTabPath; + private String serviceName; + private boolean useTicketCache; + private Path ticketCachePath; + private boolean renewTGT; + + public SPNEGOAuthentication(URI uri) + { + super(uri, ANY_REALM); + } + + @Override + public String getType() + { + return NEGOTIATE; + } + + /** + * @return the user name of the user to login + */ + public String getUserName() + { + return userName; + } + + /** + * @param userName user name of the user to login + */ + public void setUserName(String userName) + { + this.userName = userName; + } + + /** + * @return the password of the user to login + */ + public String getUserPassword() + { + return userPassword; + } + + /** + * @param userPassword the password of the user to login + * @see #setUserKeyTabPath(Path) + */ + public void setUserPassword(String userPassword) + { + this.userPassword = userPassword; + } + + /** + * @return the path of the keyTab file with the user credentials + */ + public Path getUserKeyTabPath() + { + return userKeyTabPath; + } + + /** + * @param userKeyTabPath the path of the keyTab file with the user credentials + * @see #setUserPassword(String) + */ + public void setUserKeyTabPath(Path userKeyTabPath) + { + this.userKeyTabPath = userKeyTabPath; + } + + /** + * @return the name of the service to use + */ + public String getServiceName() + { + return serviceName; + } + + /** + * @param serviceName the name of the service to use + */ + public void setServiceName(String serviceName) + { + this.serviceName = serviceName; + } + + /** + * @return whether to use the ticket cache during login + */ + public boolean isUseTicketCache() + { + return useTicketCache; + } + + /** + * @param useTicketCache whether to use the ticket cache during login + * @see #setTicketCachePath(Path) + */ + public void setUseTicketCache(boolean useTicketCache) + { + this.useTicketCache = useTicketCache; + } + + /** + * @return the path of the ticket cache file + */ + public Path getTicketCachePath() + { + return ticketCachePath; + } + + /** + * @param ticketCachePath the path of the ticket cache file + * @see #setUseTicketCache(boolean) + */ + public void setTicketCachePath(Path ticketCachePath) + { + this.ticketCachePath = ticketCachePath; + } + + /** + * @return whether to renew the ticket granting ticket + */ + public boolean isRenewTGT() + { + return renewTGT; + } + + /** + * @param renewTGT whether to renew the ticket granting ticket + */ + public void setRenewTGT(boolean renewTGT) + { + this.renewTGT = renewTGT; + } + + @Override + public Result authenticate(Request request, ContentResponse response, HeaderInfo headerInfo, Attributes context) + { + SPNEGOContext spnegoContext = (SPNEGOContext)context.getAttribute(SPNEGOContext.ATTRIBUTE); + if (LOG.isDebugEnabled()) + LOG.debug("Authenticate with context {}", spnegoContext); + if (spnegoContext == null) + { + spnegoContext = login(); + context.setAttribute(SPNEGOContext.ATTRIBUTE, spnegoContext); + } + + String b64Input = headerInfo.getBase64(); + byte[] input = b64Input == null ? new byte[0] : Base64.getDecoder().decode(b64Input); + byte[] output = Subject.doAs(spnegoContext.subject, initGSSContext(spnegoContext, request.getHost(), input)); + String b64Output = output == null ? null : new String(Base64.getEncoder().encode(output)); + + // The result cannot be used for subsequent requests, + // so it always has a null URI to avoid being cached. + return new SPNEGOResult(null, b64Output); + } + + private SPNEGOContext login() + { + try + { + // First login via JAAS using the Kerberos AS_REQ call, with a client user. + // This will populate the Subject with the client user principal and the TGT. + String user = getUserName(); + if (LOG.isDebugEnabled()) + LOG.debug("Logging in user {}", user); + CallbackHandler callbackHandler = new PasswordCallbackHandler(); + LoginContext loginContext = new LoginContext("", null, callbackHandler, new SPNEGOConfiguration()); + loginContext.login(); + Subject subject = loginContext.getSubject(); + + SPNEGOContext spnegoContext = new SPNEGOContext(); + spnegoContext.subject = subject; + if (LOG.isDebugEnabled()) + LOG.debug("Initialized {}", spnegoContext); + return spnegoContext; + } + catch (LoginException x) + { + throw new RuntimeException(x); + } + } + + private PrivilegedActionA configurable (as opposed to using system properties) SPNEGO LoginService.
+ *At startup, this LoginService will login via JAAS the service principal, composed + * of the {@link #getServiceName() service name} and the {@link #getHostName() host name}, + * for example {@code HTTP/wonder.com}, using a {@code keyTab} file as the service principal + * credentials.
+ *Upon receiving a HTTP request, the server tries to authenticate the client + * calling {@link #login(String, Object, ServletRequest)} where the GSS APIs are used to + * verify client tokens and (perhaps after a few round-trips) a {@code GSSContext} is + * established.
+ */ +public class ConfigurableSpnegoLoginService extends ContainerLifeCycle implements LoginService +{ + private static final Logger LOG = Log.getLogger(ConfigurableSpnegoLoginService.class); + + private final GSSManager _gssManager = GSSManager.getInstance(); + private final String _realm; + private final AuthorizationService _authorizationService; + private IdentityService _identityService = new DefaultIdentityService(); + private String _serviceName; + private Path _keyTabPath; + private String _hostName; + private SpnegoContext _context; + + public ConfigurableSpnegoLoginService(String realm, AuthorizationService authorizationService) + { + _realm = realm; + _authorizationService = authorizationService; + } + + /** + * @return the realm name + */ + @Override + public String getName() + { + return _realm; + } + + /** + * @return the path of the keyTab file containing service credentials + */ + public Path getKeyTabPath() + { + return _keyTabPath; + } + + /** + * @param keyTabFile the path of the keyTab file containing service credentials + */ + public void setKeyTabPath(Path keyTabFile) + { + _keyTabPath = keyTabFile; + } + + /** + * @return the service name, typically "HTTP" + * @see #getHostName() + */ + public String getServiceName() + { + return _serviceName; + } + + /** + * @param serviceName the service name + * @see #setHostName(String) + */ + public void setServiceName(String serviceName) + { + _serviceName = serviceName; + } + + /** + * @return the host name of the service + * @see #setServiceName(String) + */ + public String getHostName() + { + return _hostName; + } + + /** + * @param hostName the host name of the service + */ + public void setHostName(String hostName) + { + _hostName = hostName; + } + + @Override + protected void doStart() throws Exception + { + if (_hostName == null) + _hostName = InetAddress.getLocalHost().getCanonicalHostName(); + if (LOG.isDebugEnabled()) + LOG.debug("Retrieving credentials for service {}/{}", getServiceName(), getHostName()); + LoginContext loginContext = new LoginContext("", null, null, new SpnegoConfiguration()); + loginContext.login(); + Subject subject = loginContext.getSubject(); + _context = Subject.doAs(subject, newSpnegoContext(subject)); + super.doStart(); + } + + private PrivilegedActionA service to query for user roles.
+ */ +@FunctionalInterface +public interface AuthorizationService +{ + /** + * @param request the current HTTP request + * @param name the user name + * @return a {@link UserIdentity} to query for roles of the given user + */ + UserIdentity getUserIdentity(HttpServletRequest request, String name); + + /** + *Wraps a {@link LoginService} as an AuthorizationService
+ * + * @param loginService the {@link LoginService} to wrap + * @param credentials + * @return an AuthorizationService that delegates the query for roles to the given {@link LoginService} + */ + public static AuthorizationService from(LoginService loginService, Object credentials) + { + return (request, name) -> loginService.login(name, credentials, request); + } +} diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ConfigurableSpnegoAuthenticator.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ConfigurableSpnegoAuthenticator.java new file mode 100644 index 00000000000..6cae1a4745b --- /dev/null +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ConfigurableSpnegoAuthenticator.java @@ -0,0 +1,232 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.security.authentication; + +import java.io.IOException; +import java.io.Serializable; +import java.time.Duration; +import java.time.Instant; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.security.ServerAuthException; +import org.eclipse.jetty.security.SpnegoUserIdentity; +import org.eclipse.jetty.security.SpnegoUserPrincipal; +import org.eclipse.jetty.security.UserAuthentication; +import org.eclipse.jetty.server.Authentication; +import org.eclipse.jetty.server.Authentication.User; +import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.security.Constraint; + +/** + *A LoginAuthenticator that uses SPNEGO and the GSS API to authenticate requests.
+ *A successful authentication from a client is cached for a configurable + * {@link #getAuthenticationDuration() duration} using the HTTP session; this avoids + * that the client is asked to authenticate for every request.
+ * + * @see org.eclipse.jetty.security.ConfigurableSpnegoLoginService + */ +public class ConfigurableSpnegoAuthenticator extends LoginAuthenticator +{ + private static final Logger LOG = Log.getLogger(ConfigurableSpnegoAuthenticator.class); + + private final String _authMethod; + private Duration _authenticationDuration = Duration.ofNanos(-1); + + public ConfigurableSpnegoAuthenticator() + { + this(Constraint.__SPNEGO_AUTH); + } + + /** + * Allow for a custom authMethod value to be set for instances where SPNEGO may not be appropriate + * + * @param authMethod the auth method + */ + public ConfigurableSpnegoAuthenticator(String authMethod) + { + _authMethod = authMethod; + } + + @Override + public String getAuthMethod() + { + return _authMethod; + } + + /** + * @return the authentication duration + */ + public Duration getAuthenticationDuration() + { + return _authenticationDuration; + } + + /** + *Sets the duration of the authentication.
+ *A negative duration means that the authentication is only valid for the current request.
+ *A zero duration means that the authentication is valid forever.
+ *A positive value means that the authentication is valid for the specified duration.
+ * + * @param authenticationDuration the authentication duration + */ + public void setAuthenticationDuration(Duration authenticationDuration) + { + _authenticationDuration = authenticationDuration; + } + + @Override + public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException + { + if (!mandatory) + return new DeferredAuthentication(this); + + HttpServletRequest request = (HttpServletRequest)req; + HttpServletResponse response = (HttpServletResponse)res; + + String header = request.getHeader(HttpHeader.AUTHORIZATION.asString()); + String spnegoToken = getSpnegoToken(header); + HttpSession httpSession = request.getSession(false); + + // We have a token from the client, so run the login. + if (header != null && spnegoToken != null) + { + SpnegoUserIdentity identity = (SpnegoUserIdentity)login(null, spnegoToken, request); + if (identity.isEstablished()) + { + if (!DeferredAuthentication.isDeferred(response)) + { + if (LOG.isDebugEnabled()) + LOG.debug("Sending final token"); + // Send to the client the final token so that the + // client can establish the GSS context with the server. + SpnegoUserPrincipal principal = (SpnegoUserPrincipal)identity.getUserPrincipal(); + setSpnegoToken(response, principal.getEncodedToken()); + } + + Duration authnDuration = getAuthenticationDuration(); + if (!authnDuration.isNegative()) + { + if (httpSession == null) + httpSession = request.getSession(true); + httpSession.setAttribute(UserIdentityHolder.ATTRIBUTE, new UserIdentityHolder(identity)); + } + return new UserAuthentication(getAuthMethod(), identity); + } + else + { + if (DeferredAuthentication.isDeferred(response)) + return Authentication.UNAUTHENTICATED; + if (LOG.isDebugEnabled()) + LOG.debug("Sending intermediate challenge"); + SpnegoUserPrincipal principal = (SpnegoUserPrincipal)identity.getUserPrincipal(); + sendChallenge(response, principal.getEncodedToken()); + return Authentication.SEND_CONTINUE; + } + } + // No token from the client; check if the client has logged in + // successfully before and the authentication has not expired. + else if (httpSession != null) + { + UserIdentityHolder holder = (UserIdentityHolder)httpSession.getAttribute(UserIdentityHolder.ATTRIBUTE); + if (holder != null) + { + UserIdentity identity = holder._userIdentity; + if (identity != null) + { + Duration authnDuration = getAuthenticationDuration(); + if (!authnDuration.isNegative()) + { + boolean expired = !authnDuration.isZero() && Instant.now().isAfter(holder._validFrom.plus(authnDuration)); + // Allow non-GET requests even if they're expired, so that + // the client does not need to send the request content again. + if (!expired || !HttpMethod.GET.is(request.getMethod())) + return new UserAuthentication(getAuthMethod(), identity); + } + } + } + } + + if (DeferredAuthentication.isDeferred(response)) + return Authentication.UNAUTHENTICATED; + + if (LOG.isDebugEnabled()) + LOG.debug("Sending initial challenge"); + sendChallenge(response, null); + return Authentication.SEND_CONTINUE; + } + + private void sendChallenge(HttpServletResponse response, String token) throws ServerAuthException + { + try + { + setSpnegoToken(response, token); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } + catch (IOException x) + { + throw new ServerAuthException(x); + } + } + + private void setSpnegoToken(HttpServletResponse response, String token) + { + String value = HttpHeader.NEGOTIATE.asString(); + if (token != null) + value += " " + token; + response.setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), value); + } + + private String getSpnegoToken(String header) + { + if (header == null) + return null; + String scheme = HttpHeader.NEGOTIATE.asString() + " "; + if (header.regionMatches(true, 0, scheme, 0, scheme.length())) + return header.substring(scheme.length()).trim(); + return null; + } + + @Override + public boolean secureResponse(ServletRequest request, ServletResponse response, boolean mandatory, User validatedUser) + { + return true; + } + + private static class UserIdentityHolder implements Serializable + { + private static final String ATTRIBUTE = UserIdentityHolder.class.getName(); + + private transient final Instant _validFrom = Instant.now(); + private transient final UserIdentity _userIdentity; + + private UserIdentityHolder(UserIdentity userIdentity) + { + _userIdentity = userIdentity; + } + } +} diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticator.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticator.java index 0dfe1b6add5..8142eb7c4d8 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticator.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticator.java @@ -35,6 +35,10 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.security.Constraint; +/** + * @deprecated use {@link ConfigurableSpnegoAuthenticator} instead. + */ +@Deprecated public class SpnegoAuthenticator extends LoginAuthenticator { private static final Logger LOG = Log.getLogger(SpnegoAuthenticator.class); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java index bba38c90b6e..c1baef1553d 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java @@ -31,13 +31,18 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.MultiMap; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; public class Dispatcher implements RequestDispatcher { + private static final Logger LOG = Log.getLogger(Dispatcher.class); + public final static String __ERROR_DISPATCH="org.eclipse.jetty.server.Dispatcher.ERROR"; /** Dispatch include attribute names */ @@ -195,8 +200,27 @@ public class Dispatcher implements RequestDispatcher baseRequest.setContextPath(_contextHandler.getContextPath()); baseRequest.setServletPath(null); baseRequest.setPathInfo(_pathInContext); - if (_uri.getQuery()!=null || old_uri.getQuery()!=null) - baseRequest.mergeQueryParameters(old_uri.getQuery(),_uri.getQuery(), true); + + if (_uri.getQuery() != null || old_uri.getQuery() != null) + { + try + { + baseRequest.mergeQueryParameters(old_uri.getQuery(), _uri.getQuery(), true); + } + catch (BadMessageException e) + { + // Only throw BME if not in Error Dispatch Mode + // This allows application ErrorPageErrorHandler to handle BME messages + if (dispatch != DispatcherType.ERROR) + { + throw e; + } + else + { + LOG.warn("Ignoring Original Bad Request Query String: " + old_uri, e); + } + } + } baseRequest.setAttributes(attr); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java index e3fa2e32d81..e9560d603b2 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -201,7 +201,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu private final Listtrue
if this filter applies
*/
boolean appliesTo(int type)
@@ -295,9 +317,9 @@ public class FilterMapping implements Dumpable
public String toString()
{
return
- TypeUtil.asList(_pathSpecs)+"/"+
- TypeUtil.asList(_servletNames)+"=="+
- _dispatches+"=>"+
+ TypeUtil.asList(_pathSpecs)+"/"+
+ TypeUtil.asList(_servletNames)+"/"+
+ Arrays.stream(DispatcherType.values()).filter(this::appliesTo).collect(Collectors.toSet())+"=>"+
_filterName;
}
diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ListenerHolder.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ListenerHolder.java
index 9e94f463f62..b3aa0e4c845 100644
--- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ListenerHolder.java
+++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ListenerHolder.java
@@ -46,7 +46,12 @@ public class ListenerHolder extends BaseHolder