From 784afa25be7c740ce4fb623f6a894df9a54f4e97 Mon Sep 17 00:00:00 2001
From: Lachlan Roberts
Date: Wed, 16 Oct 2024 13:13:03 +1100
Subject: [PATCH 1/7] Issue #5442 - implement CompositeAuthenticator to allow
multiple authentication options
Signed-off-by: Lachlan Roberts
---
.../jetty/security/AnyUserLoginService.java | 109 ++++++
.../eclipse/jetty/security/Authenticator.java | 1 +
.../security/DefaultAuthenticatorFactory.java | 84 +++-
.../jetty/security/MultiAuthenticator.java | 360 ++++++++++++++++++
.../authentication/LoginAuthenticator.java | 8 +-
.../internal/DeferredAuthenticationState.java | 3 +-
.../security/openid/OpenIdConfiguration.java | 2 +-
tests/test-integration/pom.xml | 13 +
.../jetty/test/MultiAuthenticatorTest.java | 219 +++++++++++
.../src/test/resources/user.properties | 2 +
10 files changed, 778 insertions(+), 23 deletions(-)
create mode 100644 jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/AnyUserLoginService.java
create mode 100644 jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/MultiAuthenticator.java
create mode 100644 tests/test-integration/src/test/java/org/eclipse/jetty/test/MultiAuthenticatorTest.java
create mode 100644 tests/test-integration/src/test/resources/user.properties
diff --git a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/AnyUserLoginService.java b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/AnyUserLoginService.java
new file mode 100644
index 000000000000..b45636a069e2
--- /dev/null
+++ b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/AnyUserLoginService.java
@@ -0,0 +1,109 @@
+//
+// ========================================================================
+// 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.util.function.Function;
+import javax.security.auth.Subject;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Session;
+
+/**
+ * A {@link LoginService} which allows unknown users to be authenticated.
+ *
+ * This can delegate to a nested {@link LoginService} if it is supplied to the constructor, it 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)}.
+ *
+ */
+public class AnyUserLoginService implements LoginService
+{
+ private final String _realm;
+ private final LoginService _loginService;
+ private IdentityService _identityService;
+
+ /**
+ * @param realm the realm name.
+ * @param loginService optional {@link LoginService} which can be used to assign roles to known users.
+ */
+ public AnyUserLoginService(String realm, LoginService loginService)
+ {
+ _realm = realm;
+ _loginService = loginService;
+ _identityService = (loginService == null) ? new DefaultIdentityService() : null;
+ }
+
+ @Override
+ public String getName()
+ {
+ return _realm;
+ }
+
+ @Override
+ public UserIdentity login(String username, Object credentials, Request request, Function getOrCreateSession)
+ {
+ if (_loginService != null)
+ {
+ UserIdentity login = _loginService.login(username, credentials, request, getOrCreateSession);
+ if (login != null)
+ return login;
+
+ UserPrincipal userPrincipal = new UserPrincipal(username, null);
+ Subject subject = new Subject();
+ subject.getPrincipals().add(userPrincipal);
+ if (credentials != null)
+ subject.getPrivateCredentials().add(credentials);
+ subject.setReadOnly();
+ return _loginService.getUserIdentity(subject, userPrincipal, true);
+ }
+
+ UserPrincipal userPrincipal = new UserPrincipal(username, null);
+ Subject subject = new Subject();
+ subject.getPrincipals().add(userPrincipal);
+ if (credentials != null)
+ subject.getPrivateCredentials().add(credentials);
+ subject.setReadOnly();
+ return _identityService.newUserIdentity(subject, userPrincipal, new String[0]);
+ }
+
+ @Override
+ public boolean validate(UserIdentity user)
+ {
+ if (_loginService == null)
+ return user != null;
+ return _loginService.validate(user);
+ }
+
+ @Override
+ public IdentityService getIdentityService()
+ {
+ return _loginService == null ? _identityService : _loginService.getIdentityService();
+ }
+
+ @Override
+ public void setIdentityService(IdentityService service)
+ {
+ if (_loginService != null)
+ _loginService.setIdentityService(service);
+ else
+ _identityService = service;
+ }
+
+ @Override
+ public void logout(UserIdentity user)
+ {
+ if (_loginService != null)
+ _loginService.logout(user);
+ }
+}
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..54a099a8434b 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,71 @@ 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());
+ return switch (auth)
+ {
+ 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;
+ };
+ }
- 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))
+ 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))
{
- 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());
- }
+ 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();
- return authenticator;
+ 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..6c3669434ff4
--- /dev/null
+++ b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/MultiAuthenticator.java
@@ -0,0 +1,360 @@
+package org.eclipse.jetty.security;
+
+import java.security.Principal;
+import java.util.function.Function;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.pathmap.MatchedResource;
+import org.eclipse.jetty.http.pathmap.PathMappings;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MultiAuthenticator extends LoginAuthenticator
+{
+ private static final Logger LOG = LoggerFactory.getLogger(MultiAuthenticator.class);
+
+ private static final String AUTH_STATE_ATTR = MultiAuthState.class.getName();
+ private static final DefaultAuthenticator DEFAULT_AUTHENTICATOR = new DefaultAuthenticator();
+ private final PathMappings _authenticatorsMappings = new PathMappings<>();
+
+ public void addAuthenticator(String pathSpec, Authenticator authenticator)
+ {
+ _authenticatorsMappings.put(pathSpec, authenticator);
+ }
+
+ @Override
+ public void setConfiguration(Configuration configuration)
+ {
+ for (Authenticator authenticator : _authenticatorsMappings.values())
+ {
+ authenticator.setConfiguration(configuration);
+ }
+ }
+
+ @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)
+ 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);
+ else
+ 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 = DEFAULT_AUTHENTICATOR;
+ 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)
+ return authenticationState;
+
+ // Wrap the successful authentication state to intercept the logout request to clear the session attribute.
+ if (authenticationState instanceof AuthenticationState.Succeeded succeededState)
+ {
+ if (succeededState instanceof LoginAuthenticator.UserAuthenticationSent)
+ doLogin(request);
+ 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 static class DefaultAuthenticator implements Authenticator
+ {
+
+ @Override
+ public void setConfiguration(Configuration configuration)
+ {
+ }
+
+ @Override
+ public String getAuthenticationType()
+ {
+ return "DEFAULT";
+ }
+
+ @Override
+ public AuthenticationState validateRequest(Request request, Response response, Callback callback)
+ {
+ 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 (DEFAULT_AUTHENTICATOR.getClass().getName().equals(name))
+ return DEFAULT_AUTHENTICATOR;
+ for (Authenticator authenticator : _authenticatorsMappings.values())
+ {
+ if (name.equals(authenticator.getClass().getName()))
+ return authenticator;
+ }
+
+ return null;
+ }
+ }
+
+ private static class MultiAuthState
+ {
+ 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-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..498b8d18d455
--- /dev/null
+++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/MultiAuthenticatorTest.java
@@ -0,0 +1,219 @@
+package org.eclipse.jetty.test;
+
+import java.io.PrintWriter;
+import java.nio.file.Path;
+import java.util.Map;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.http.HttpHeader;
+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;
+
+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);
+ _connector.setPort(8080); // TODO: remove.
+ _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();
+
+ 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 test() throws Exception
+ {
+ _server.join();
+ }
+
+ @Test
+ public void test2() throws Exception
+ {
+ _server.join();
+ }
+
+ private static class AuthTestHandler extends Handler.Abstract
+ {
+ @Override
+ public boolean handle(Request request, Response response, Callback callback) throws Exception
+ {
+ String pathInContext = Request.getPathInContext(request);
+ if (pathInContext.startsWith("/error"))
+ return onError(request, response, callback);
+ else if (pathInContext.startsWith("/logout"))
+ return onLogout(request, response, callback);
+ else if (pathInContext.startsWith("/login/form"))
+ return onFormLogin(request, response, callback);
+
+ try (PrintWriter writer = new PrintWriter(Content.Sink.asOutputStream(response)))
+ {
+ AuthenticationState authenticationState = AuthenticationState.getAuthenticationState(request);
+ response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/html");
+ writer.println("authState: " + authenticationState + "
");
+ if (authenticationState instanceof AuthenticationState.Deferred deferred)
+ {
+ AuthenticationState.Succeeded succeeded = deferred.authenticate(request);
+ if (succeeded != null)
+ writer.println("userPrincipal: " + succeeded.getUserPrincipal() + "
");
+ else
+ writer.println("userPrincipal: null
");
+ }
+ else if (authenticationState != null)
+ {
+ writer.println("userPrincipal: " + authenticationState.getUserPrincipal() + "
");
+ }
+
+ Session session = request.getSession(true);
+ @SuppressWarnings("unchecked")
+ Map claims = (Map)session.getAttribute(OpenIdAuthenticator.CLAIMS);
+ if (claims != null)
+ {
+ writer.printf("""
+
Authenticated with OpenID
+ userId: %s
+ name: %s
+ email: %s
+ """, claims.get("sub"), claims.get("name"), claims.get("email"));
+ }
+
+ writer.println("""
+ OpenID Login
+ Form Login
+ Logout
+ """);
+ }
+
+ callback.succeeded();
+ return true;
+ }
+
+ private boolean onFormLogin(Request request, Response response, Callback callback) throws Exception
+ {
+ String content = """
+ Login
+
+ """;
+ response.write(true, BufferUtil.toBuffer(content), callback);
+ return true;
+ }
+
+ private boolean onLogout(Request request, Response response, Callback callback) throws Exception
+ {
+ Request.AuthenticationState authState = Request.getAuthenticationState(request);
+ if (authState instanceof AuthenticationState.Succeeded succeeded)
+ succeeded.logout(request, response);
+ else if (authState instanceof AuthenticationState.Deferred deferred)
+ deferred.logout(request, response);
+ else
+ request.getSession(true).invalidate();
+
+ if (!response.isCommitted())
+ Response.sendRedirect(request, response, callback, "/");
+ else
+ callback.succeeded();
+ return true;
+ }
+
+ private boolean onError(Request request, Response response, Callback callback) throws Exception
+ {
+ Fields parameters = Request.getParameters(request);
+ String errorDescription = parameters.getValue("error_description_jetty");
+ response.write(true, BufferUtil.toBuffer("error: " + errorDescription), callback);
+ return true;
+ }
+ }
+}
diff --git a/tests/test-integration/src/test/resources/user.properties b/tests/test-integration/src/test/resources/user.properties
new file mode 100644
index 000000000000..f988a961c291
--- /dev/null
+++ b/tests/test-integration/src/test/resources/user.properties
@@ -0,0 +1,2 @@
+user=password
+admin=password
From 9d0d9c4d3ad470c61bf26a34b5080df05f8ce1b9 Mon Sep 17 00:00:00 2001
From: Lachlan Roberts
Date: Wed, 16 Oct 2024 13:52:32 +1100
Subject: [PATCH 2/7] Issue #5442 - combined login page for MultiAuthenticator
Signed-off-by: Lachlan Roberts
---
.../jetty/security/MultiAuthenticator.java | 100 +++++++++++++++---
.../jetty/test/MultiAuthenticatorTest.java | 13 +++
2 files changed, 100 insertions(+), 13 deletions(-)
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
index 6c3669434ff4..90c1810e4c8b 100644
--- 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
@@ -4,6 +4,7 @@
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.security.authentication.LoginAuthenticator;
@@ -11,16 +12,20 @@
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;
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 static final DefaultAuthenticator DEFAULT_AUTHENTICATOR = new DefaultAuthenticator();
+ private final DefaultAuthenticator _defaultAuthenticator = new DefaultAuthenticator();
private final PathMappings _authenticatorsMappings = new PathMappings<>();
+ private String _loginPath;
+ private boolean _dispatch;
public void addAuthenticator(String pathSpec, Authenticator authenticator)
{
@@ -30,12 +35,52 @@ public void addAuthenticator(String pathSpec, Authenticator 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);
}
}
+ 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()
{
@@ -47,7 +92,11 @@ public UserIdentity login(String username, Object password, Request request, Res
{
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);
}
@@ -56,9 +105,12 @@ public void logout(Request request, Response response)
{
Authenticator authenticator = getAuthenticator(request.getSession(false));
if (authenticator instanceof LoginAuthenticator loginAuthenticator)
+ {
loginAuthenticator.logout(request, response);
- else
- super.logout(request, response);
+ doLogout(request);
+ }
+
+ super.logout(request, response);
}
@Override
@@ -80,7 +132,7 @@ public Constraint.Authorization getConstraintAuthentication(String pathInContext
if (authenticator == null)
authenticator = getAuthenticator(session);
if (authenticator == null)
- authenticator = DEFAULT_AUTHENTICATOR;
+ authenticator = _defaultAuthenticator;
saveAuthenticator(session, authenticator);
return authenticator.getConstraintAuthentication(pathInContext, existing, getSession);
}
@@ -98,15 +150,14 @@ public AuthenticationState validateRequest(Request request, Response response, C
AuthenticationState authenticationState = authenticator.validateRequest(request, response, callback);
if (authenticationState instanceof AuthenticationState.ResponseSent)
+ {
+ if (authenticationState instanceof LoginAuthenticator.UserAuthenticationSent)
+ doLogin(request);
return authenticationState;
+ }
- // Wrap the successful authentication state to intercept the logout request to clear the session attribute.
if (authenticationState instanceof AuthenticationState.Succeeded succeededState)
- {
- if (succeededState instanceof LoginAuthenticator.UserAuthenticationSent)
- doLogin(request);
return new MultiSucceededAuthenticationState(succeededState);
- }
else if (authenticationState instanceof AuthenticationState.Deferred deferredState)
return new MultiDelegateAuthenticationState(deferredState);
return authenticationState;
@@ -213,9 +264,8 @@ public void logout(Request request, Response response)
}
}
- private static class DefaultAuthenticator implements Authenticator
+ private class DefaultAuthenticator implements Authenticator
{
-
@Override
public void setConfiguration(Configuration configuration)
{
@@ -227,9 +277,33 @@ 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;
}
}
@@ -316,8 +390,8 @@ private Authenticator getAuthenticator(Session session)
return null;
String name = state.getAuthenticatorName();
- if (DEFAULT_AUTHENTICATOR.getClass().getName().equals(name))
- return DEFAULT_AUTHENTICATOR;
+ if (_defaultAuthenticator.getClass().getName().equals(name))
+ return _defaultAuthenticator;
for (Authenticator authenticator : _authenticatorsMappings.values())
{
if (name.equals(authenticator.getClass().getName()))
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
index 498b8d18d455..b82e9be917c3 100644
--- 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
@@ -1,3 +1,16 @@
+//
+// ========================================================================
+// 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;
From 713ab7fe69f8ae4189b00187273cdc01c2795ce9 Mon Sep 17 00:00:00 2001
From: Lachlan Roberts
Date: Wed, 16 Oct 2024 17:49:47 +1100
Subject: [PATCH 3/7] Issue #5442 - update the tests for MultiAuthenticator
Signed-off-by: Lachlan Roberts
---
.../jetty/test/MultiAuthenticatorTest.java | 144 +++++++++++++-----
1 file changed, 109 insertions(+), 35 deletions(-)
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
index b82e9be917c3..0430fc0da0c4 100644
--- 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
@@ -14,11 +14,15 @@
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;
@@ -48,6 +52,10 @@
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;
@@ -64,7 +72,6 @@ public void before() throws Exception
_server = new Server();
_connector = new ServerConnector(_server);
- _connector.setPort(8080); // TODO: remove.
_server.addConnector(_connector);
OpenIdConfiguration config = new OpenIdConfiguration(_provider.getProvider(), _provider.getClientId(), _provider.getClientSecret());
@@ -77,6 +84,7 @@ public void before() throws Exception
securityHandler.setHandler(new AuthTestHandler());
MultiAuthenticator multiAuthenticator = new MultiAuthenticator();
+ multiAuthenticator.setLoginPath("/login");
OpenIdAuthenticator openIdAuthenticator = new OpenIdAuthenticator(config, "/error");
openIdAuthenticator.setRedirectPath("/redirect_path");
@@ -116,15 +124,63 @@ public void after() throws Exception
}
@Test
- public void test() throws Exception
+ public void testMultiAuthentication() throws Exception
{
- _server.join();
+ 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("
+ * 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/MultiAuthenticator.java b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/MultiAuthenticator.java
index 1deed6f036c7..ccb1aeaddb56 100644
--- 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
@@ -9,6 +9,7 @@
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;
@@ -18,6 +19,14 @@
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);
@@ -29,6 +38,11 @@ public class MultiAuthenticator extends LoginAuthenticator
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);
@@ -47,6 +61,10 @@ public void setConfiguration(Configuration 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)
From 4a69ed30a3c08d1518d07cf33aa09865c0f1e8c6 Mon Sep 17 00:00:00 2001
From: Lachlan Roberts
Date: Tue, 7 Jan 2025 18:25:09 +1100
Subject: [PATCH 7/7] PR #12393 - update javadoc and remove duplicate for
AnyUserLoginService
Signed-off-by: Lachlan Roberts
---
.../jetty/security/AnyUserLoginService.java | 8 +-
.../security/siwe/EthereumAuthenticator.java | 2 +-
.../siwe/internal/AnyUserLoginService.java | 114 ------------------
3 files changed, 7 insertions(+), 117 deletions(-)
delete mode 100644 jetty-integrations/jetty-ethereum/src/main/java/org/eclipse/jetty/security/siwe/internal/AnyUserLoginService.java
diff --git a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/AnyUserLoginService.java b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/AnyUserLoginService.java
index eb758be2b7e6..031717d0fbef 100644
--- a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/AnyUserLoginService.java
+++ b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/AnyUserLoginService.java
@@ -21,10 +21,14 @@
/**
* 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)}.
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-ethereum/src/main/java/org/eclipse/jetty/security/siwe/internal/AnyUserLoginService.java b/jetty-integrations/jetty-ethereum/src/main/java/org/eclipse/jetty/security/siwe/internal/AnyUserLoginService.java
deleted file mode 100644
index e1f099cda87b..000000000000
--- a/jetty-integrations/jetty-ethereum/src/main/java/org/eclipse/jetty/security/siwe/internal/AnyUserLoginService.java
+++ /dev/null
@@ -1,114 +0,0 @@
-//
-// ========================================================================
-// 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.siwe.internal;
-
-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 can delegate to a nested {@link LoginService} if it is supplied to the constructor, it 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)}.
- *
- */
-public class AnyUserLoginService implements LoginService
-{
- private final String _realm;
- private final LoginService _loginService;
- private IdentityService _identityService;
-
- /**
- * @param realm the realm name.
- * @param loginService optional {@link LoginService} which can be used to assign roles to known users.
- */
- public AnyUserLoginService(String realm, LoginService loginService)
- {
- _realm = realm;
- _loginService = loginService;
- _identityService = (loginService == null) ? new DefaultIdentityService() : null;
- }
-
- @Override
- public String getName()
- {
- return _realm;
- }
-
- @Override
- public UserIdentity login(String username, Object credentials, Request request, Function getOrCreateSession)
- {
- if (_loginService != null)
- {
- UserIdentity login = _loginService.login(username, credentials, request, getOrCreateSession);
- if (login != null)
- return login;
-
- UserPrincipal userPrincipal = new UserPrincipal(username, null);
- Subject subject = new Subject();
- subject.getPrincipals().add(userPrincipal);
- if (credentials != null)
- subject.getPrivateCredentials().add(credentials);
- subject.setReadOnly();
- return _loginService.getUserIdentity(subject, userPrincipal, true);
- }
-
- UserPrincipal userPrincipal = new UserPrincipal(username, null);
- Subject subject = new Subject();
- subject.getPrincipals().add(userPrincipal);
- if (credentials != null)
- subject.getPrivateCredentials().add(credentials);
- subject.setReadOnly();
- return _identityService.newUserIdentity(subject, userPrincipal, new String[0]);
- }
-
- @Override
- public boolean validate(UserIdentity user)
- {
- if (_loginService == null)
- return user != null;
- return _loginService.validate(user);
- }
-
- @Override
- public IdentityService getIdentityService()
- {
- return _loginService == null ? _identityService : _loginService.getIdentityService();
- }
-
- @Override
- public void setIdentityService(IdentityService service)
- {
- if (_loginService != null)
- _loginService.setIdentityService(service);
- else
- _identityService = service;
- }
-
- @Override
- public void logout(UserIdentity user)
- {
- if (_loginService != null)
- _loginService.logout(user);
- }
-}