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("
    ")); + + // Form login is successful. + Fields fields = new Fields(); + fields.put("j_username", "user"); + fields.put("j_password", "password"); + response = _client.POST(uri.resolve("/j_security_check")) + .body(new FormRequestContent(fields)) + .send(); + assertThat(response.getStatus(), equalTo(HttpStatus.OK_200)); + assertThat(response.getContentAsString(), containsString("userPrincipal: user")); + assertThat(response.getContentAsString(), containsString("MultiAuthenticator$MultiSucceededAuthenticationState")); + + // Logout is successful. + response = _client.GET(uri.resolve("/logout")); + 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")); + + // We can now log in with OpenID. + _provider.setUser(new OpenIdProvider.User("UserId1234", "openIdUser")); + response = _client.GET(uri.resolve("/login/openid")); + assertThat(response.getStatus(), equalTo(HttpStatus.OK_200)); + assertThat(response.getContentAsString(), containsString("userPrincipal: UserId1234")); + assertThat(response.getContentAsString(), containsString("Authenticated with OpenID")); + assertThat(response.getContentAsString(), containsString("name: openIdUser")); + + // Logout is successful. + response = _client.GET(uri.resolve("/logout")); + 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")); } - @Test - public void test2() throws Exception + private static AuthenticationState.Succeeded getAuthentication(Request request) { - _server.join(); + AuthenticationState authenticationState = AuthenticationState.getAuthenticationState(request); + AuthenticationState.Succeeded auth = null; + if (authenticationState instanceof AuthenticationState.Succeeded succeeded) + auth = succeeded; + else if (authenticationState instanceof AuthenticationState.Deferred deferred) + auth = deferred.authenticate(request); + return auth; } private static class AuthTestHandler extends Handler.Abstract @@ -139,51 +195,67 @@ else if (pathInContext.startsWith("/logout")) return onLogout(request, response, callback); else if (pathInContext.startsWith("/login/form")) return onFormLogin(request, response, callback); + else if (pathInContext.startsWith("/login/openid")) + return onOpenIdLogin(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 auth = getAuthentication(request); + if (auth != null) { - 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() + "
    "); - } + writer.println("authState: " + auth + "
    "); + writer.println("userPrincipal: " + auth.getUserPrincipal() + "
    "); - Session session = request.getSession(true); - @SuppressWarnings("unchecked") - Map claims = (Map)session.getAttribute(OpenIdAuthenticator.CLAIMS); - if (claims != null) + 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(""" +
    + Logout
    + """); + } + else { - writer.printf(""" -
    Authenticated with OpenID
    - userId: %s
    - name: %s
    - email: %s
    - """, claims.get("sub"), claims.get("name"), claims.get("email")); + writer.println(""" +

    Multi Login Page

    + OpenID Login
    + Form Login
    + Logout
    + """); } - - writer.println(""" - OpenID Login
    - Form Login
    - Logout
    - """); } callback.succeeded(); return true; } + private boolean onOpenIdLogin(Request request, Response response, Callback callback) throws Exception + { + Response.sendRedirect(request, response, callback, "/"); + return true; + } + private boolean onFormLogin(Request request, Response response, Callback callback) throws Exception { + AuthenticationState.Succeeded authentication = getAuthentication(request); + if (authentication != null) + { + Response.sendRedirect(request, response, callback, "/"); + return true; + } + String content = """

    Login

    @@ -199,6 +271,8 @@ private boolean onFormLogin(Request request, Response response, Callback callbac +

    Username: user or admin
    + Password: password

    """; response.write(true, BufferUtil.toBuffer(content), callback); return true; From 2bfb4c69b70d96810ff14717781e8e461677de74 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 16 Oct 2024 18:06:28 +1100 Subject: [PATCH 4/7] Issue #5442 - fix bug in DefaultAuthenticatorFactory Signed-off-by: Lachlan Roberts --- .../eclipse/jetty/security/DefaultAuthenticatorFactory.java | 3 +++ 1 file changed, 3 insertions(+) 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 54a099a8434b..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 @@ -56,6 +56,9 @@ public class DefaultAuthenticatorFactory implements Authenticator.Factory public Authenticator getAuthenticator(Server server, Context context, Configuration configuration) { String auth = StringUtil.asciiToUpperCase(configuration.getAuthenticationType()); + if (auth == null) + return null; + return switch (auth) { case Authenticator.BASIC_AUTH -> new BasicAuthenticator(); From 11aed53c81daa0ad88123c05c7d65dd9205c2f1a Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 22 Oct 2024 16:53:21 +1100 Subject: [PATCH 5/7] PR #12393 - implement serializable for MultiAuthState Signed-off-by: Lachlan Roberts --- .../org/eclipse/jetty/security/MultiAuthenticator.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 90c1810e4c8b..1deed6f036c7 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 @@ -1,5 +1,7 @@ package org.eclipse.jetty.security; +import java.io.Serial; +import java.io.Serializable; import java.security.Principal; import java.util.function.Function; @@ -402,8 +404,11 @@ private Authenticator getAuthenticator(Session session) } } - private static class MultiAuthState + private static class MultiAuthState implements Serializable { + @Serial + private static final long serialVersionUID = -4292431864385753482L; + private String _authenticatorName; private boolean _isLoggedIn; From 1b0437cc5e23433b093cb7b799f8771fd4f1978a Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Fri, 25 Oct 2024 11:13:54 +1100 Subject: [PATCH 6/7] PR #12393 - adding javadoc from review Signed-off-by: Lachlan Roberts --- .../jetty/security/AnyUserLoginService.java | 2 ++ .../jetty/security/MultiAuthenticator.java | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) 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 b45636a069e2..eb758be2b7e6 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 @@ -26,6 +26,8 @@ * with the nested {@link LoginService} and only create a new {@link UserIdentity} if none was found with * {@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/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); - } -}