diff --git a/jetty-integrations/jetty-ethereum/src/main/java/org/eclipse/jetty/security/siwe/internal/AnyUserLoginService.java b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/AnyUserLoginService.java
similarity index 82%
rename from jetty-integrations/jetty-ethereum/src/main/java/org/eclipse/jetty/security/siwe/internal/AnyUserLoginService.java
rename to jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/AnyUserLoginService.java
index e1f099cda87b..031717d0fbef 100644
--- a/jetty-integrations/jetty-ethereum/src/main/java/org/eclipse/jetty/security/siwe/internal/AnyUserLoginService.java
+++ b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/AnyUserLoginService.java
@@ -11,26 +11,27 @@
// ========================================================================
//
-package org.eclipse.jetty.security.siwe.internal;
+package org.eclipse.jetty.security;
import java.util.function.Function;
import javax.security.auth.Subject;
-import org.eclipse.jetty.security.DefaultIdentityService;
-import org.eclipse.jetty.security.IdentityService;
-import org.eclipse.jetty.security.LoginService;
-import org.eclipse.jetty.security.UserIdentity;
-import org.eclipse.jetty.security.UserPrincipal;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Session;
/**
* A {@link LoginService} which allows unknown users to be authenticated.
+ *
This is useful for authentication protocols like OpenID Connect and Sign in With Ethereum, where Jetty doesn't store
+ * a collection of user credentials and passwords. Once the user proves authenticates themselves through the respective
+ * protocol, Jetty does not have to validate any credential.
*
- * This can delegate to a nested {@link LoginService} if it is supplied to the constructor, it will first attempt to log in
+ * This can delegate to a nested {@link LoginService} which can supply roles for known users.
+ * This nested {@link LoginService} is supplied to the constructor, and this will first attempt to log in
* with the nested {@link LoginService} and only create a new {@link UserIdentity} if none was found with
- * {@link LoginService#login(String, Object, Request, Function)}.
+ * {@link LoginService#login(String, Object, Request, Function)}
*
+ * This {@link LoginService} does not check credentials, a {@link UserIdentity} will be produced for any
+ * username provided in {@link #login(String, Object, Request, Function)}.
*/
public class AnyUserLoginService implements LoginService
{
diff --git a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/Authenticator.java b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/Authenticator.java
index 210187dd046c..87c8c5f6f511 100644
--- a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/Authenticator.java
+++ b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/Authenticator.java
@@ -43,6 +43,7 @@ public interface Authenticator
String NEGOTIATE_AUTH = "NEGOTIATE";
String OPENID_AUTH = "OPENID";
String SIWE_AUTH = "SIWE";
+ String MULTI_AUTH = "MULTI";
/**
* Configure the Authenticator
diff --git a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultAuthenticatorFactory.java b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultAuthenticatorFactory.java
index a47eed687486..a9ef7d5a926b 100644
--- a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultAuthenticatorFactory.java
+++ b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultAuthenticatorFactory.java
@@ -14,6 +14,7 @@
package org.eclipse.jetty.security;
import java.util.Collection;
+import java.util.List;
import org.eclipse.jetty.security.Authenticator.Configuration;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
@@ -26,6 +27,7 @@
import org.eclipse.jetty.security.internal.DeferredAuthenticationState;
import org.eclipse.jetty.server.Context;
import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.ssl.SslContextFactory;
/**
@@ -53,27 +55,74 @@ public class DefaultAuthenticatorFactory implements Authenticator.Factory
@Override
public Authenticator getAuthenticator(Server server, Context context, Configuration configuration)
{
- String auth = configuration.getAuthenticationType();
- Authenticator authenticator = null;
+ String auth = StringUtil.asciiToUpperCase(configuration.getAuthenticationType());
+ if (auth == null)
+ return null;
- if (Authenticator.BASIC_AUTH.equalsIgnoreCase(auth))
- authenticator = new BasicAuthenticator();
- else if (Authenticator.DIGEST_AUTH.equalsIgnoreCase(auth))
- authenticator = new DigestAuthenticator();
- else if (Authenticator.FORM_AUTH.equalsIgnoreCase(auth))
- authenticator = new FormAuthenticator();
- else if (Authenticator.SPNEGO_AUTH.equalsIgnoreCase(auth))
- authenticator = new SPNEGOAuthenticator();
- else if (Authenticator.NEGOTIATE_AUTH.equalsIgnoreCase(auth)) // see Bug #377076
- authenticator = new SPNEGOAuthenticator(Authenticator.NEGOTIATE_AUTH);
- if (Authenticator.CERT_AUTH2.equalsIgnoreCase(auth))
+ return switch (auth)
{
- Collection sslContextFactories = server.getBeans(SslContextFactory.class);
- if (sslContextFactories.size() != 1)
- throw new IllegalStateException("SslClientCertAuthenticator requires a single SslContextFactory instances.");
- authenticator = new SslClientCertAuthenticator(sslContextFactories.iterator().next());
- }
+ case Authenticator.BASIC_AUTH -> new BasicAuthenticator();
+ case Authenticator.DIGEST_AUTH -> new DigestAuthenticator();
+ case Authenticator.FORM_AUTH -> new FormAuthenticator();
+ case Authenticator.SPNEGO_AUTH -> new SPNEGOAuthenticator();
+ case Authenticator.NEGOTIATE_AUTH -> new SPNEGOAuthenticator(Authenticator.NEGOTIATE_AUTH); // see Bug #377076
+ case Authenticator.MULTI_AUTH -> getMultiAuthenticator(server, context, configuration);
+ case Authenticator.CERT_AUTH, Authenticator.CERT_AUTH2 ->
+ {
+ Collection sslContextFactories = server.getBeans(SslContextFactory.class);
+ if (sslContextFactories.size() != 1)
+ throw new IllegalStateException("SslClientCertAuthenticator requires a single SslContextFactory instances.");
+ yield new SslClientCertAuthenticator(sslContextFactories.iterator().next());
+ }
+ default -> null;
+ };
+ }
- return authenticator;
+ private Authenticator getMultiAuthenticator(Server server, Context context, Authenticator.Configuration configuration)
+ {
+ SecurityHandler securityHandler = SecurityHandler.getCurrentSecurityHandler();
+ if (securityHandler == null)
+ return null;
+
+ String auth = configuration.getAuthenticationType();
+ if (Authenticator.MULTI_AUTH.equalsIgnoreCase(auth))
+ {
+ MultiAuthenticator multiAuthenticator = new MultiAuthenticator();
+
+ String authenticatorConfig = configuration.getParameter("org.eclipse.jetty.security.multi.authenticators");
+ for (String config : StringUtil.csvSplit(authenticatorConfig))
+ {
+ String[] parts = config.split(":");
+ if (parts.length != 2)
+ throw new IllegalArgumentException();
+
+ String authType = parts[0].trim();
+ String pathSpec = parts[1].trim();
+
+ Authenticator.Configuration.Wrapper authConfig = new Authenticator.Configuration.Wrapper(configuration)
+ {
+ @Override
+ public String getAuthenticationType()
+ {
+ return authType;
+ }
+ };
+
+ Authenticator authenticator = null;
+ List authenticatorFactories = securityHandler.getKnownAuthenticatorFactories();
+ for (Authenticator.Factory factory : authenticatorFactories)
+ {
+ authenticator = factory.getAuthenticator(server, context, authConfig);
+ if (authenticator != null)
+ break;
+ }
+
+ if (authenticator == null)
+ throw new IllegalStateException();
+ multiAuthenticator.addAuthenticator(pathSpec, authenticator);
+ }
+ return multiAuthenticator;
+ }
+ return null;
}
}
diff --git a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/MultiAuthenticator.java b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/MultiAuthenticator.java
new file mode 100644
index 000000000000..ccb1aeaddb56
--- /dev/null
+++ b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/MultiAuthenticator.java
@@ -0,0 +1,457 @@
+package org.eclipse.jetty.security;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.security.Principal;
+import java.util.function.Function;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.http.pathmap.MatchedResource;
+import org.eclipse.jetty.http.pathmap.PathMappings;
+import org.eclipse.jetty.http.pathmap.PathSpec;
+import org.eclipse.jetty.security.authentication.LoginAuthenticator;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.server.Session;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.URIUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An {@link Authenticator} which maps different {@link Authenticator}s to {@link PathSpec}s.
+ * This can be used to support multiple different authentication methods for a single application such as
+ * FORM, OPENID and SIWE.
+ * The {@link #setLoginPath(String)} can be used to set a login page where unauthenticated users are
+ * redirected in the case that no {@link Authenticator}s were matched. This can be used as a page to
+ * link to other paths where {@link Authenticator}s are mapped to so that users can choose their login method.
+ */
+public class MultiAuthenticator extends LoginAuthenticator
+{
+ private static final Logger LOG = LoggerFactory.getLogger(MultiAuthenticator.class);
+ public static final String LOGIN_PATH_PARAM = "org.eclipse.jetty.security.multi.login_path";
+
+ private static final String AUTH_STATE_ATTR = MultiAuthState.class.getName();
+ private final DefaultAuthenticator _defaultAuthenticator = new DefaultAuthenticator();
+ private final PathMappings _authenticatorsMappings = new PathMappings<>();
+ private String _loginPath;
+ private boolean _dispatch;
+
+ /**
+ * Adds an authenticator which maps to the given pathSpec.
+ * @param pathSpec the pathSpec.
+ * @param authenticator the authenticator.
+ */
+ public void addAuthenticator(String pathSpec, Authenticator authenticator)
+ {
+ _authenticatorsMappings.put(pathSpec, authenticator);
+ }
+
+ @Override
+ public void setConfiguration(Configuration configuration)
+ {
+ String loginPath = configuration.getParameter(LOGIN_PATH_PARAM);
+ if (loginPath != null)
+ setLoginPath(loginPath);
+
+ for (Authenticator authenticator : _authenticatorsMappings.values())
+ {
+ authenticator.setConfiguration(configuration);
+ }
+ }
+
+ /**
+ * If a user is unauthenticated, a request which does not map to any of the {@link Authenticator}s will redirect to this path.
+ * @param loginPath the loginPath.
+ */
+ public void setLoginPath(String loginPath)
+ {
+ if (loginPath != null)
+ {
+ if (!loginPath.startsWith("/"))
+ {
+ LOG.warn("login path must start with /");
+ loginPath = "/" + loginPath;
+ }
+
+ _loginPath = loginPath;
+ }
+ }
+
+ public boolean isLoginPage(String uri)
+ {
+ return matchURI(uri, _loginPath);
+ }
+
+ private boolean matchURI(String uri, String path)
+ {
+ int jsc = uri.indexOf(path);
+ if (jsc < 0)
+ return false;
+ int e = jsc + path.length();
+ if (e == uri.length())
+ return true;
+ char c = uri.charAt(e);
+ return c == ';' || c == '#' || c == '/' || c == '?';
+ }
+
+ public void setDispatch(boolean dispatch)
+ {
+ _dispatch = dispatch;
+ }
+
+ @Override
+ public String getAuthenticationType()
+ {
+ return "MULTI";
+ }
+
+ @Override
+ public UserIdentity login(String username, Object password, Request request, Response response)
+ {
+ Authenticator authenticator = getAuthenticator(request.getSession(false));
+ if (authenticator instanceof LoginAuthenticator loginAuthenticator)
+ {
+ doLogin(request);
+ return loginAuthenticator.login(username, password, request, response);
+ }
+
+ return super.login(username, password, request, response);
+ }
+
+ @Override
+ public void logout(Request request, Response response)
+ {
+ Authenticator authenticator = getAuthenticator(request.getSession(false));
+ if (authenticator instanceof LoginAuthenticator loginAuthenticator)
+ {
+ loginAuthenticator.logout(request, response);
+ doLogout(request);
+ }
+
+ super.logout(request, response);
+ }
+
+ @Override
+ public Constraint.Authorization getConstraintAuthentication(String pathInContext, Constraint.Authorization existing, Function getSession)
+ {
+ Session session = getSession.apply(true);
+
+ // If we are logged in we should always use that authenticator until logged out.
+ if (isLoggedIn(session))
+ {
+ Authenticator authenticator = getAuthenticator(session);
+ return authenticator.getConstraintAuthentication(pathInContext, existing, getSession);
+ }
+
+ Authenticator authenticator = null;
+ MatchedResource matched = _authenticatorsMappings.getMatched(pathInContext);
+ if (matched != null)
+ authenticator = matched.getResource();
+ if (authenticator == null)
+ authenticator = getAuthenticator(session);
+ if (authenticator == null)
+ authenticator = _defaultAuthenticator;
+ saveAuthenticator(session, authenticator);
+ return authenticator.getConstraintAuthentication(pathInContext, existing, getSession);
+ }
+
+ @Override
+ public AuthenticationState validateRequest(Request request, Response response, Callback callback) throws ServerAuthException
+ {
+ Session session = request.getSession(true);
+ Authenticator authenticator = getAuthenticator(session);
+ if (authenticator == null)
+ {
+ Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403);
+ return AuthenticationState.SEND_FAILURE;
+ }
+
+ AuthenticationState authenticationState = authenticator.validateRequest(request, response, callback);
+ if (authenticationState instanceof AuthenticationState.ResponseSent)
+ {
+ if (authenticationState instanceof LoginAuthenticator.UserAuthenticationSent)
+ doLogin(request);
+ return authenticationState;
+ }
+
+ if (authenticationState instanceof AuthenticationState.Succeeded succeededState)
+ return new MultiSucceededAuthenticationState(succeededState);
+ else if (authenticationState instanceof AuthenticationState.Deferred deferredState)
+ return new MultiDelegateAuthenticationState(deferredState);
+ return authenticationState;
+ }
+
+ @Override
+ public Request prepareRequest(Request request, AuthenticationState authenticationState)
+ {
+ Session session = request.getSession(true);
+ Authenticator authenticator = getAuthenticator(session);
+ if (authenticator == null)
+ throw new IllegalStateException("No authenticator found");
+ return authenticator.prepareRequest(request, authenticationState);
+ }
+
+ private static class MultiDelegateAuthenticationState implements AuthenticationState.Deferred
+ {
+ private final AuthenticationState.Deferred _delegate;
+
+ public MultiDelegateAuthenticationState(AuthenticationState.Deferred state)
+ {
+ _delegate = state;
+ }
+
+ @Override
+ public Succeeded authenticate(Request request)
+ {
+ return _delegate.authenticate(request);
+ }
+
+ @Override
+ public AuthenticationState authenticate(Request request, Response response, Callback callback)
+ {
+ return _delegate.authenticate(request, response, callback);
+ }
+
+ @Override
+ public Succeeded login(String username, Object password, Request request, Response response)
+ {
+ Succeeded succeeded = _delegate.login(username, password, request, response);
+ if (succeeded != null)
+ doLogin(request);
+ return succeeded;
+ }
+
+ @Override
+ public void logout(Request request, Response response)
+ {
+ _delegate.logout(request, response);
+ doLogout(request);
+ }
+
+ @Override
+ public IdentityService.Association getAssociation()
+ {
+ return _delegate.getAssociation();
+ }
+
+ @Override
+ public Principal getUserPrincipal()
+ {
+ return _delegate.getUserPrincipal();
+ }
+ }
+
+ private static class MultiSucceededAuthenticationState implements AuthenticationState.Succeeded
+ {
+ private final AuthenticationState.Succeeded _delegate;
+
+ public MultiSucceededAuthenticationState(AuthenticationState.Succeeded state)
+ {
+ _delegate = state;
+ }
+
+ @Override
+ public String getAuthenticationType()
+ {
+ return _delegate.getAuthenticationType();
+ }
+
+ @Override
+ public UserIdentity getUserIdentity()
+ {
+ return _delegate.getUserIdentity();
+ }
+
+ @Override
+ public Principal getUserPrincipal()
+ {
+ return _delegate.getUserPrincipal();
+ }
+
+ @Override
+ public boolean isUserInRole(String role)
+ {
+ return _delegate.isUserInRole(role);
+ }
+
+ @Override
+ public void logout(Request request, Response response)
+ {
+ _delegate.logout(request, response);
+ doLogout(request);
+ }
+ }
+
+ private class DefaultAuthenticator implements Authenticator
+ {
+ @Override
+ public void setConfiguration(Configuration configuration)
+ {
+ }
+
+ @Override
+ public String getAuthenticationType()
+ {
+ return "DEFAULT";
+ }
+
+ @Override
+ public Constraint.Authorization getConstraintAuthentication(String pathInContext, Constraint.Authorization existing, Function getSession)
+ {
+ if (isLoginPage(pathInContext))
+ return Constraint.Authorization.ALLOWED;
+ return existing;
+ }
+
+ @Override
+ public AuthenticationState validateRequest(Request request, Response response, Callback callback)
+ {
+ if (_loginPath != null)
+ {
+ String loginPath = URIUtil.addPaths(request.getContext().getContextPath(), _loginPath);
+ if (_dispatch)
+ {
+ HttpURI.Mutable newUri = HttpURI.build(request.getHttpURI()).pathQuery(loginPath);
+ return new AuthenticationState.ServeAs(newUri);
+ }
+ else
+ {
+ Session session = request.getSession(true);
+ String redirectUri = session.encodeURI(request, loginPath, true);
+ Response.sendRedirect(request, response, callback, redirectUri, true);
+ return AuthenticationState.CHALLENGE;
+ }
+ }
+ return null;
+ }
+ }
+
+ private static MultiAuthState getAuthState(Session session)
+ {
+ if (session == null)
+ return null;
+ return (MultiAuthState)session.getAttribute(AUTH_STATE_ATTR);
+ }
+
+ private static MultiAuthState ensureAuthState(Session session)
+ {
+ if (session == null)
+ throw new IllegalArgumentException();
+
+ MultiAuthState authState = (MultiAuthState)session.getAttribute(AUTH_STATE_ATTR);
+ if (authState == null)
+ {
+ authState = new MultiAuthState();
+ session.setAttribute(AUTH_STATE_ATTR, authState);
+ }
+ return authState;
+ }
+
+ private static boolean isLoggedIn(Session session)
+ {
+ if (session == null)
+ return false;
+
+ synchronized (session)
+ {
+ MultiAuthState authState = getAuthState(session);
+ return authState != null && authState.isLoggedIn();
+ }
+ }
+
+ private static void doLogin(Request request)
+ {
+ Session session = request.getSession(true);
+ if (session != null)
+ {
+ synchronized (session)
+ {
+ MultiAuthState authState = ensureAuthState(session);
+ authState.setLogin(true);
+ }
+ }
+ }
+
+ private static void doLogout(Request request)
+ {
+ Session session = request.getSession(false);
+ if (session != null)
+ {
+ synchronized (session)
+ {
+ session.removeAttribute(AUTH_STATE_ATTR);
+ }
+ }
+ }
+
+ private void saveAuthenticator(Session session, Authenticator authenticator)
+ {
+ if (session == null)
+ throw new IllegalArgumentException();
+
+ synchronized (session)
+ {
+ MultiAuthState authState = ensureAuthState(session);
+ authState.setAuthenticatorName(authenticator.getClass().getName());
+ }
+ }
+
+ private Authenticator getAuthenticator(Session session)
+ {
+ if (session == null)
+ return null;
+
+ synchronized (session)
+ {
+ MultiAuthState state = getAuthState(session);
+ if (state == null || state.getAuthenticatorName() == null)
+ return null;
+
+ String name = state.getAuthenticatorName();
+ if (_defaultAuthenticator.getClass().getName().equals(name))
+ return _defaultAuthenticator;
+ for (Authenticator authenticator : _authenticatorsMappings.values())
+ {
+ if (name.equals(authenticator.getClass().getName()))
+ return authenticator;
+ }
+
+ return null;
+ }
+ }
+
+ private static class MultiAuthState implements Serializable
+ {
+ @Serial
+ private static final long serialVersionUID = -4292431864385753482L;
+
+ private String _authenticatorName;
+ private boolean _isLoggedIn;
+
+ public MultiAuthState()
+ {
+ }
+
+ public void setAuthenticatorName(String authenticatorName)
+ {
+ _authenticatorName = authenticatorName;
+ }
+
+ public String getAuthenticatorName()
+ {
+ return _authenticatorName;
+ }
+
+ public void setLogin(boolean isLoggedIn)
+ {
+ _isLoggedIn = isLoggedIn;
+ }
+
+ private boolean isLoggedIn()
+ {
+ return _isLoggedIn;
+ }
+ }
+}
diff --git a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginAuthenticator.java b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginAuthenticator.java
index bf0dbc1a1f1e..acfabe94c0e0 100644
--- a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginAuthenticator.java
+++ b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginAuthenticator.java
@@ -82,7 +82,8 @@ public void logout(Request request, Response response)
@Override
public void setConfiguration(Configuration configuration)
{
- _loginService = configuration.getLoginService();
+ if (_loginService == null)
+ _loginService = configuration.getLoginService();
if (_loginService == null)
throw new IllegalStateException("No LoginService for " + this + " in " + configuration);
_identityService = configuration.getIdentityService();
@@ -97,6 +98,11 @@ public LoginService getLoginService()
return _loginService;
}
+ public void setLoginService(LoginService loginService)
+ {
+ _loginService = loginService;
+ }
+
/**
* Update the session on authentication.
* The session is changed to a new instance with a new ID if and only if:
diff --git a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/internal/DeferredAuthenticationState.java b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/internal/DeferredAuthenticationState.java
index 50725117160c..828997c4380d 100644
--- a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/internal/DeferredAuthenticationState.java
+++ b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/internal/DeferredAuthenticationState.java
@@ -56,8 +56,7 @@ public Succeeded authenticate(Request request)
if (authenticationState instanceof Succeeded succeeded)
{
LoginService loginService = _authenticator.getLoginService();
- IdentityService identityService = loginService.getIdentityService();
-
+ IdentityService identityService = loginService == null ? null : loginService.getIdentityService();
if (identityService != null)
{
UserIdentity user = succeeded.getUserIdentity();
diff --git a/jetty-integrations/jetty-ethereum/src/main/java/org/eclipse/jetty/security/siwe/EthereumAuthenticator.java b/jetty-integrations/jetty-ethereum/src/main/java/org/eclipse/jetty/security/siwe/EthereumAuthenticator.java
index d5eb997eec65..a39a392f9393 100644
--- a/jetty-integrations/jetty-ethereum/src/main/java/org/eclipse/jetty/security/siwe/EthereumAuthenticator.java
+++ b/jetty-integrations/jetty-ethereum/src/main/java/org/eclipse/jetty/security/siwe/EthereumAuthenticator.java
@@ -34,6 +34,7 @@
import org.eclipse.jetty.http.MultiPartFormData;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.ByteBufferContentSource;
+import org.eclipse.jetty.security.AnyUserLoginService;
import org.eclipse.jetty.security.AuthenticationState;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.Constraint;
@@ -42,7 +43,6 @@
import org.eclipse.jetty.security.UserIdentity;
import org.eclipse.jetty.security.authentication.LoginAuthenticator;
import org.eclipse.jetty.security.authentication.SessionAuthentication;
-import org.eclipse.jetty.security.siwe.internal.AnyUserLoginService;
import org.eclipse.jetty.security.siwe.internal.EthereumUtil;
import org.eclipse.jetty.security.siwe.internal.SignInWithEthereumToken;
import org.eclipse.jetty.server.FormFields;
diff --git a/jetty-integrations/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java b/jetty-integrations/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java
index fcd9049c990d..f61105237466 100644
--- a/jetty-integrations/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java
+++ b/jetty-integrations/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java
@@ -109,7 +109,7 @@ public OpenIdConfiguration(@Name("issuer") String issuer,
* @param issuer The URL of the OpenID provider.
* @param authorizationEndpoint the URL of the OpenID provider's authorization endpoint if configured.
* @param tokenEndpoint the URL of the OpenID provider's token endpoint if configured.
- * @param endSessionEndpoint the URL of the OpdnID provider's end session endpoint if configured.
+ * @param endSessionEndpoint the URL of the OpenID provider's end session endpoint if configured.
* @param clientId OAuth 2.0 Client Identifier valid at the Authorization Server.
* @param clientSecret The client secret known only by the Client and the Authorization Server.
* @param authenticationMethod Authentication method to use with the Token Endpoint.
diff --git a/tests/test-integration/pom.xml b/tests/test-integration/pom.xml
index e15a14a5b55a..a1fafe099388 100644
--- a/tests/test-integration/pom.xml
+++ b/tests/test-integration/pom.xml
@@ -18,10 +18,18 @@
org.eclipse.jetty
jetty-client
+
+ org.eclipse.jetty
+ jetty-openid
+
org.eclipse.jetty
jetty-server
+
+ org.eclipse.jetty
+ jetty-session
+
org.slf4j
slf4j-api
@@ -31,6 +39,11 @@
jetty-slf4j-impl
test
+
+ org.eclipse.jetty.tests
+ jetty-test-common
+ test
+
org.eclipse.jetty.toolchain
jetty-test-helper
diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/MultiAuthenticatorTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/MultiAuthenticatorTest.java
new file mode 100644
index 000000000000..0430fc0da0c4
--- /dev/null
+++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/MultiAuthenticatorTest.java
@@ -0,0 +1,306 @@
+//
+// ========================================================================
+// 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.test;
+
+import java.io.PrintWriter;
+import java.net.URI;
+import java.nio.file.Path;
+import java.util.Map;
+
+import org.eclipse.jetty.client.ContentResponse;
+import org.eclipse.jetty.client.FormRequestContent;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.io.Content;
+import org.eclipse.jetty.security.AnyUserLoginService;
+import org.eclipse.jetty.security.AuthenticationState;
+import org.eclipse.jetty.security.Constraint;
+import org.eclipse.jetty.security.HashLoginService;
+import org.eclipse.jetty.security.MultiAuthenticator;
+import org.eclipse.jetty.security.SecurityHandler;
+import org.eclipse.jetty.security.authentication.FormAuthenticator;
+import org.eclipse.jetty.security.openid.OpenIdAuthenticator;
+import org.eclipse.jetty.security.openid.OpenIdConfiguration;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.Session;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.session.SessionHandler;
+import org.eclipse.jetty.tests.OpenIdProvider;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.Fields;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+
+public class MultiAuthenticatorTest
+{
+ private Server _server;
+ private ServerConnector _connector;
+ private HttpClient _client;
+ private OpenIdProvider _provider;
+
+ @BeforeEach
+ public void before() throws Exception
+ {
+ // Set up a local OIDC provider and add its configuration to the Server.
+ _provider = new OpenIdProvider();
+ _provider.start();
+
+ _server = new Server();
+ _connector = new ServerConnector(_server);
+ _server.addConnector(_connector);
+
+ OpenIdConfiguration config = new OpenIdConfiguration(_provider.getProvider(), _provider.getClientId(), _provider.getClientSecret());
+ _server.addBean(config);
+
+ SecurityHandler.PathMapped securityHandler = new SecurityHandler.PathMapped();
+ securityHandler.put("", Constraint.ALLOWED);
+ securityHandler.put("/logout", Constraint.ALLOWED);
+ securityHandler.put("/", Constraint.ANY_USER);
+ securityHandler.setHandler(new AuthTestHandler());
+
+ MultiAuthenticator multiAuthenticator = new MultiAuthenticator();
+ multiAuthenticator.setLoginPath("/login");
+
+ OpenIdAuthenticator openIdAuthenticator = new OpenIdAuthenticator(config, "/error");
+ openIdAuthenticator.setRedirectPath("/redirect_path");
+ openIdAuthenticator.setLogoutRedirectPath("/");
+ multiAuthenticator.addAuthenticator("/login/openid", openIdAuthenticator);
+
+ Path fooPropsFile = MavenTestingUtils.getTestResourcePathFile("user.properties");
+ Resource fooResource = ResourceFactory.root().newResource(fooPropsFile);
+ HashLoginService loginService = new HashLoginService("users", fooResource);
+ _server.addBean(loginService);
+ FormAuthenticator formAuthenticator = new FormAuthenticator("/login/form", "/error", false);
+ formAuthenticator.setLoginService(loginService);
+ multiAuthenticator.addAuthenticator("/login/form", formAuthenticator);
+
+ securityHandler.setAuthenticator(multiAuthenticator);
+ securityHandler.setLoginService(new AnyUserLoginService(_provider.getProvider(), null));
+ SessionHandler sessionHandler = new SessionHandler();
+ sessionHandler.setHandler(securityHandler);
+ ContextHandler contextHandler = new ContextHandler();
+ contextHandler.setContextPath("/");
+ contextHandler.setHandler(sessionHandler);
+
+ _server.setHandler(contextHandler);
+ _server.start();
+ String redirectUri = "http://localhost:" + _connector.getLocalPort() + "/redirect_path";
+ _provider.addRedirectUri(redirectUri);
+
+ _client = new HttpClient();
+ _client.start();
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ _client.stop();
+ _server.stop();
+ }
+
+ @Test
+ public void testMultiAuthentication() throws Exception
+ {
+ URI uri = URI.create("http://localhost:" + _connector.getLocalPort());
+ ContentResponse response = _client.GET(uri);
+ assertThat(response.getStatus(), equalTo(HttpStatus.OK_200));
+ assertThat(response.getContentAsString(), containsString("Multi Login Page
"));
+ assertThat(response.getContentAsString(), containsString("/login/openid"));
+ assertThat(response.getContentAsString(), containsString("/login/form"));
+
+ // Try Form Login.
+ response = _client.GET(uri.resolve("/login/form"));
+ assertThat(response.getStatus(), equalTo(HttpStatus.OK_200));
+ assertThat(response.getContentAsString(), containsString("