Merge pull request #7043 from eclipse/jetty-10.0.x-7042-OpenIdConfiguration

Issue #7042 - Allow OpenIdConfiguration to be selected based on realm name
This commit is contained in:
Lachlan 2021-11-23 16:11:22 +11:00 committed by GitHub
commit d755e3a742
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 546 additions and 110 deletions

View File

@ -20,33 +20,32 @@
</Arg> </Arg>
<Set name="executor"><Ref refid="ThreadPool"/></Set> <Set name="executor"><Ref refid="ThreadPool"/></Set>
</New> </New>
<New id="OpenIdConfiguration" class="org.eclipse.jetty.security.openid.OpenIdConfiguration">
<Arg><Property name="jetty.openid.provider" deprecated="jetty.openid.openIdProvider"/></Arg>
<Arg><Property name="jetty.openid.provider.authorizationEndpoint"/></Arg>
<Arg><Property name="jetty.openid.provider.tokenEndpoint"/></Arg>
<Arg><Property name="jetty.openid.clientId"/></Arg>
<Arg><Property name="jetty.openid.clientSecret"/></Arg>
<Arg><Property name="jetty.openid.authMethod" default="client_secret_post"/></Arg>
<Arg><Ref refid="HttpClient"/></Arg>
<Call name="addScopes">
<Arg>
<Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
<Arg><Property name="jetty.openid.scopes"/></Arg>
</Call>
</Arg>
</Call>
</New>
<Call name="addBean"> <Call name="addBean">
<Arg> <Arg>
<New class="org.eclipse.jetty.security.openid.OpenIdLoginService"> <Ref refid="BaseLoginService"/>
<Arg><Ref refid="OpenIdConfiguration"/></Arg> </Arg>
<Arg><Ref refid="BaseLoginService"/></Arg> </Call>
<Call name="setAuthenticateNewUsers"> <Call name="addBean">
<Arg type="boolean"> <Arg>
<Property name="jetty.openid.authenticateNewUsers" default="false"/> <New id="OpenIdConfiguration" class="org.eclipse.jetty.security.openid.OpenIdConfiguration">
<Arg name="issuer"><Property name="jetty.openid.provider" deprecated="jetty.openid.openIdProvider"/></Arg>
<Arg name="authorizationEndpoint"><Property name="jetty.openid.provider.authorizationEndpoint"/></Arg>
<Arg name="tokenEndpoint"><Property name="jetty.openid.provider.tokenEndpoint"/></Arg>
<Arg name="clientId"><Property name="jetty.openid.clientId"/></Arg>
<Arg name="clientSecret"><Property name="jetty.openid.clientSecret"/></Arg>
<Arg name="authMethod"><Property name="jetty.openid.authMethod" default="client_secret_post"/></Arg>
<Arg name="httpClient"><Ref refid="HttpClient"/></Arg>
<Set name="authenticateNewUsers">
<Property name="jetty.openid.authenticateNewUsers" default="false"/>
</Set>
<Call name="addScopes">
<Arg>
<Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
<Arg><Property name="jetty.openid.scopes"/></Arg>
</Call>
</Arg> </Arg>
</Call> </Call>
</New> </New>
</Arg> </Arg>
</Call> </Call>
</Configure> </Configure>

View File

@ -20,7 +20,7 @@ etc/jetty-openid.xml
[ini-template] [ini-template]
## The OpenID Identity Provider's issuer ID (the entire URL *before* ".well-known/openid-configuration") ## The OpenID Identity Provider's issuer ID (the entire URL *before* ".well-known/openid-configuration")
# jetty.openid.provider=https://id.example.com/~ # jetty.openid.provider=https://id.example.com/
## The OpenID Identity Provider's authorization endpoint (optional if the metadata of the OP is accessible) ## The OpenID Identity Provider's authorization endpoint (optional if the metadata of the OP is accessible)
# jetty.openid.provider.authorizationEndpoint=https://id.example.com/authorization # jetty.openid.provider.authorizationEndpoint=https://id.example.com/authorization

View File

@ -11,6 +11,8 @@
// ======================================================================== // ========================================================================
// //
import org.eclipse.jetty.security.openid.OpenIdAuthenticatorFactory;
module org.eclipse.jetty.security.openid module org.eclipse.jetty.security.openid
{ {
requires org.eclipse.jetty.util.ajax; requires org.eclipse.jetty.util.ajax;
@ -19,4 +21,6 @@ module org.eclipse.jetty.security.openid
requires transitive org.eclipse.jetty.security; requires transitive org.eclipse.jetty.security;
exports org.eclipse.jetty.security.openid; exports org.eclipse.jetty.security.openid;
provides org.eclipse.jetty.security.Authenticator.Factory with OpenIdAuthenticatorFactory;
} }

View File

@ -0,0 +1,52 @@
//
// ========================================================================
// Copyright (c) 1995-2021 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.openid;
import org.eclipse.jetty.security.Authenticator.AuthConfiguration;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.WrappedAuthConfiguration;
/**
* <p>This class is used to wrap the {@link AuthConfiguration} given to the {@link OpenIdAuthenticator}.</p>
* <p>When {@link #getLoginService()} method is called, this implementation will always return an instance of
* {@link OpenIdLoginService}. This allows you to configure an {@link OpenIdAuthenticator} using a {@code null}
* LoginService or any alternative LoginService implementation which will be wrapped by the OpenIdLoginService</p>
*/
public class OpenIdAuthConfiguration extends WrappedAuthConfiguration
{
private final OpenIdLoginService _openIdLoginService;
public OpenIdAuthConfiguration(OpenIdConfiguration openIdConfiguration, AuthConfiguration authConfiguration)
{
super(authConfiguration);
LoginService loginService = authConfiguration.getLoginService();
if (loginService instanceof OpenIdLoginService)
{
_openIdLoginService = (OpenIdLoginService)loginService;
}
else
{
_openIdLoginService = new OpenIdLoginService(openIdConfiguration, loginService);
if (loginService == null)
_openIdLoginService.setIdentityService(authConfiguration.getIdentityService());
}
}
@Override
public LoginService getLoginService()
{
return _openIdLoginService;
}
}

View File

@ -51,7 +51,7 @@ import org.slf4j.LoggerFactory;
* <p>Implements authentication using OpenId Connect on top of OAuth 2.0. * <p>Implements authentication using OpenId Connect on top of OAuth 2.0.
* *
* <p>The OpenIdAuthenticator redirects unauthenticated requests to the OpenID Connect Provider. The End-User is * <p>The OpenIdAuthenticator redirects unauthenticated requests to the OpenID Connect Provider. The End-User is
* eventually redirected back with an Authorization Code to the /j_security_check URI within the context. * eventually redirected back with an Authorization Code to the path set by {@link #setRedirectPath(String)} within the context.
* The Authorization Code is then used to authenticate the user through the {@link OpenIdCredentials} and {@link OpenIdLoginService}. * The Authorization Code is then used to authenticate the user through the {@link OpenIdCredentials} and {@link OpenIdLoginService}.
* </p> * </p>
* <p> * <p>
@ -66,6 +66,8 @@ public class OpenIdAuthenticator extends LoginAuthenticator
public static final String CLAIMS = "org.eclipse.jetty.security.openid.claims"; public static final String CLAIMS = "org.eclipse.jetty.security.openid.claims";
public static final String RESPONSE = "org.eclipse.jetty.security.openid.response"; public static final String RESPONSE = "org.eclipse.jetty.security.openid.response";
public static final String ISSUER = "org.eclipse.jetty.security.openid.issuer";
public static final String REDIRECT_PATH = "org.eclipse.jetty.security.openid.redirect_path";
public static final String ERROR_PAGE = "org.eclipse.jetty.security.openid.error_page"; public static final String ERROR_PAGE = "org.eclipse.jetty.security.openid.error_page";
public static final String J_URI = "org.eclipse.jetty.security.openid.URI"; public static final String J_URI = "org.eclipse.jetty.security.openid.URI";
public static final String J_POST = "org.eclipse.jetty.security.openid.POST"; public static final String J_POST = "org.eclipse.jetty.security.openid.POST";
@ -78,7 +80,8 @@ public class OpenIdAuthenticator extends LoginAuthenticator
public static final String CSRF_TOKEN = "org.eclipse.jetty.security.openid.csrf_token"; public static final String CSRF_TOKEN = "org.eclipse.jetty.security.openid.csrf_token";
private final SecureRandom _secureRandom = new SecureRandom(); private final SecureRandom _secureRandom = new SecureRandom();
private OpenIdConfiguration _configuration; private OpenIdConfiguration _openIdConfiguration;
private String _redirectPath;
private String _errorPage; private String _errorPage;
private String _errorPath; private String _errorPath;
private String _errorQuery; private String _errorQuery;
@ -86,31 +89,47 @@ public class OpenIdAuthenticator extends LoginAuthenticator
public OpenIdAuthenticator() public OpenIdAuthenticator()
{ {
this(null, J_SECURITY_CHECK, null);
}
public OpenIdAuthenticator(OpenIdConfiguration configuration)
{
this(configuration, J_SECURITY_CHECK, null);
} }
public OpenIdAuthenticator(OpenIdConfiguration configuration, String errorPage) public OpenIdAuthenticator(OpenIdConfiguration configuration, String errorPage)
{ {
this._configuration = configuration; this(configuration, J_SECURITY_CHECK, errorPage);
}
public OpenIdAuthenticator(OpenIdConfiguration configuration, String redirectPath, String errorPage)
{
_openIdConfiguration = configuration;
setRedirectPath(redirectPath);
if (errorPage != null) if (errorPage != null)
setErrorPage(errorPage); setErrorPage(errorPage);
} }
@Override @Override
public void setConfiguration(AuthConfiguration configuration) public void setConfiguration(AuthConfiguration authConfig)
{ {
super.setConfiguration(configuration); if (_openIdConfiguration == null)
{
LoginService loginService = authConfig.getLoginService();
if (!(loginService instanceof OpenIdLoginService))
throw new IllegalArgumentException("invalid LoginService " + loginService);
this._openIdConfiguration = ((OpenIdLoginService)loginService).getConfiguration();
}
String error = configuration.getInitParameter(ERROR_PAGE); String redirectPath = authConfig.getInitParameter(REDIRECT_PATH);
if (redirectPath != null)
_redirectPath = redirectPath;
String error = authConfig.getInitParameter(ERROR_PAGE);
if (error != null) if (error != null)
setErrorPage(error); setErrorPage(error);
if (_configuration != null) super.setConfiguration(new OpenIdAuthConfiguration(_openIdConfiguration, authConfig));
return;
LoginService loginService = configuration.getLoginService();
if (!(loginService instanceof OpenIdLoginService))
throw new IllegalArgumentException("invalid LoginService");
this._configuration = ((OpenIdLoginService)loginService).getConfiguration();
} }
@Override @Override
@ -131,7 +150,23 @@ public class OpenIdAuthenticator extends LoginAuthenticator
return _alwaysSaveUri; return _alwaysSaveUri;
} }
private void setErrorPage(String path) public void setRedirectPath(String redirectPath)
{
if (redirectPath == null)
{
LOG.warn("redirect path must not be null, defaulting to " + J_SECURITY_CHECK);
redirectPath = J_SECURITY_CHECK;
}
else if (!redirectPath.startsWith("/"))
{
LOG.warn("redirect path must start with /");
redirectPath = "/" + redirectPath;
}
_redirectPath = redirectPath;
}
public void setErrorPage(String path)
{ {
if (path == null || path.trim().length() == 0) if (path == null || path.trim().length() == 0)
{ {
@ -174,6 +209,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
session.setAttribute(SessionAuthentication.__J_AUTHENTICATED, cached); session.setAttribute(SessionAuthentication.__J_AUTHENTICATED, cached);
session.setAttribute(CLAIMS, ((OpenIdCredentials)credentials).getClaims()); session.setAttribute(CLAIMS, ((OpenIdCredentials)credentials).getClaims());
session.setAttribute(RESPONSE, ((OpenIdCredentials)credentials).getResponse()); session.setAttribute(RESPONSE, ((OpenIdCredentials)credentials).getResponse());
session.setAttribute(ISSUER, _openIdConfiguration.getIssuer());
} }
} }
return user; return user;
@ -445,11 +481,11 @@ public class OpenIdAuthenticator extends LoginAuthenticator
public boolean isJSecurityCheck(String uri) public boolean isJSecurityCheck(String uri)
{ {
int jsc = uri.indexOf(J_SECURITY_CHECK); int jsc = uri.indexOf(_redirectPath);
if (jsc < 0) if (jsc < 0)
return false; return false;
int e = jsc + J_SECURITY_CHECK.length(); int e = jsc + _redirectPath.length();
if (e == uri.length()) if (e == uri.length())
return true; return true;
char c = uri.charAt(e); char c = uri.charAt(e);
@ -467,7 +503,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
URIUtil.appendSchemeHostPort(redirectUri, request.getScheme(), URIUtil.appendSchemeHostPort(redirectUri, request.getScheme(),
request.getServerName(), request.getServerPort()); request.getServerName(), request.getServerPort());
redirectUri.append(request.getContextPath()); redirectUri.append(request.getContextPath());
redirectUri.append(J_SECURITY_CHECK); redirectUri.append(_redirectPath);
return redirectUri.toString(); return redirectUri.toString();
} }
@ -484,13 +520,13 @@ public class OpenIdAuthenticator extends LoginAuthenticator
// any custom scopes requested from configuration // any custom scopes requested from configuration
StringBuilder scopes = new StringBuilder(); StringBuilder scopes = new StringBuilder();
for (String s : _configuration.getScopes()) for (String s : _openIdConfiguration.getScopes())
{ {
scopes.append(" ").append(s); scopes.append(" ").append(s);
} }
return _configuration.getAuthEndpoint() + return _openIdConfiguration.getAuthEndpoint() +
"?client_id=" + UrlEncoded.encodeString(_configuration.getClientId(), StandardCharsets.UTF_8) + "?client_id=" + UrlEncoded.encodeString(_openIdConfiguration.getClientId(), StandardCharsets.UTF_8) +
"&redirect_uri=" + UrlEncoded.encodeString(getRedirectUri(request), StandardCharsets.UTF_8) + "&redirect_uri=" + UrlEncoded.encodeString(getRedirectUri(request), StandardCharsets.UTF_8) +
"&scope=openid" + UrlEncoded.encodeString(scopes.toString(), StandardCharsets.UTF_8) + "&scope=openid" + UrlEncoded.encodeString(scopes.toString(), StandardCharsets.UTF_8) +
"&state=" + antiForgeryToken + "&state=" + antiForgeryToken +

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.security.openid; package org.eclipse.jetty.security.openid;
import java.util.Collection;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import org.eclipse.jetty.security.Authenticator; import org.eclipse.jetty.security.Authenticator;
@ -28,7 +29,29 @@ public class OpenIdAuthenticatorFactory implements Authenticator.Factory
{ {
String auth = configuration.getAuthMethod(); String auth = configuration.getAuthMethod();
if (Constraint.__OPENID_AUTH.equalsIgnoreCase(auth)) if (Constraint.__OPENID_AUTH.equalsIgnoreCase(auth))
return new OpenIdAuthenticator(); {
// If we have an OpenIdLoginService we can extract the configuration.
if (loginService instanceof OpenIdLoginService)
return new OpenIdAuthenticator(((OpenIdLoginService)loginService).getConfiguration());
// Otherwise we should find an OpenIdConfiguration for this realm on the Server.
Collection<OpenIdConfiguration> configurations = server.getBeans(OpenIdConfiguration.class);
if (configurations == null || configurations.isEmpty())
throw new IllegalStateException("No OpenIdConfiguration found");
// If only 1 configuration use that regardless of its realm name.
if (configurations.size() == 1)
return new OpenIdAuthenticator(configurations.iterator().next());
// If there are multiple configurations then select one matching the realm name.
String realmName = configuration.getRealmName();
OpenIdConfiguration openIdConfiguration = configurations.stream()
.filter(c -> c.getIssuer().equals(realmName))
.findAny()
.orElseThrow(() -> new IllegalStateException("No OpenIdConfiguration found for realm \"" + realmName + "\""));
return new OpenIdAuthenticator(openIdConfiguration);
}
return null; return null;
} }
} }

View File

@ -48,6 +48,7 @@ public class OpenIdConfiguration extends ContainerLifeCycle
private final String authMethod; private final String authMethod;
private String authEndpoint; private String authEndpoint;
private String tokenEndpoint; private String tokenEndpoint;
private boolean authenticateNewUsers = false;
/** /**
* Create an OpenID configuration for a specific OIDC provider. * Create an OpenID configuration for a specific OIDC provider.
@ -139,29 +140,28 @@ public class OpenIdConfiguration extends ContainerLifeCycle
provider = provider.substring(0, provider.length() - 1); provider = provider.substring(0, provider.length() - 1);
Map<String, Object> result; Map<String, Object> result;
String responseBody = httpClient.GET(provider + CONFIG_PATH) String responseBody = httpClient.GET(provider + CONFIG_PATH).getContentAsString();
.getContentAsString();
Object parsedResult = new JSON().fromJSON(responseBody); Object parsedResult = new JSON().fromJSON(responseBody);
if (parsedResult instanceof Map) if (parsedResult instanceof Map)
{ {
Map<?, ?> rawResult = (Map<?, ?>)parsedResult; Map<?, ?> rawResult = (Map<?, ?>)parsedResult;
result = rawResult.entrySet().stream() result = rawResult.entrySet().stream()
.filter(entry -> entry.getValue() != null)
.collect(Collectors.toMap(it -> it.getKey().toString(), Map.Entry::getValue)); .collect(Collectors.toMap(it -> it.getKey().toString(), Map.Entry::getValue));
if (LOG.isDebugEnabled())
LOG.debug("discovery document {}", result);
return result;
} }
else else
{ {
LOG.warn("OpenID provider did not return a proper JSON object response. Result was '{}'", responseBody); LOG.warn("OpenID provider did not return a proper JSON object response. Result was '{}'", responseBody);
throw new IllegalStateException("Could not parse OpenID provider's malformed response"); throw new IllegalStateException("Could not parse OpenID provider's malformed response");
} }
LOG.debug("discovery document {}", result);
return result;
} }
catch (Exception e) catch (Exception e)
{ {
throw new IllegalArgumentException("invalid identity provider", e); throw new IllegalArgumentException("invalid identity provider " + provider, e);
} }
} }
@ -210,4 +210,21 @@ public class OpenIdConfiguration extends ContainerLifeCycle
{ {
return scopes; return scopes;
} }
public boolean isAuthenticateNewUsers()
{
return authenticateNewUsers;
}
public void setAuthenticateNewUsers(boolean authenticateNewUsers)
{
this.authenticateNewUsers = authenticateNewUsers;
}
@Override
public String toString()
{
return String.format("%s@%x{iss=%s, clientId=%s, authEndpoint=%s, authMethod=%s, tokenEndpoint=%s, scopes=%s, authNewUsers=%s}",
getClass().getSimpleName(), hashCode(), issuer, clientId, authEndpoint, authMethod, tokenEndpoint, scopes, authenticateNewUsers);
}
} }

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.security.openid; package org.eclipse.jetty.security.openid;
import java.util.Objects;
import javax.security.auth.Subject; import javax.security.auth.Subject;
import javax.servlet.ServletRequest; import javax.servlet.ServletRequest;
@ -53,10 +54,12 @@ public class OpenIdLoginService extends ContainerLifeCycle implements LoginServi
*/ */
public OpenIdLoginService(OpenIdConfiguration configuration, LoginService loginService) public OpenIdLoginService(OpenIdConfiguration configuration, LoginService loginService)
{ {
this.configuration = configuration; this.configuration = Objects.requireNonNull(configuration);
this.loginService = loginService; this.loginService = loginService;
addBean(this.configuration); addBean(this.configuration);
addBean(this.loginService); addBean(this.loginService);
setAuthenticateNewUsers(configuration.isAuthenticateNewUsers());
} }
@Override @Override
@ -93,13 +96,14 @@ public class OpenIdLoginService extends ContainerLifeCycle implements LoginServi
subject.getPrivateCredentials().add(credentials); subject.getPrivateCredentials().add(credentials);
subject.setReadOnly(); subject.setReadOnly();
IdentityService identityService = getIdentityService();
if (loginService != null) if (loginService != null)
{ {
UserIdentity userIdentity = loginService.login(openIdCredentials.getUserId(), "", req); UserIdentity userIdentity = loginService.login(openIdCredentials.getUserId(), "", req);
if (userIdentity == null) if (userIdentity == null)
{ {
if (isAuthenticateNewUsers()) if (isAuthenticateNewUsers())
return getIdentityService().newUserIdentity(subject, userPrincipal, new String[0]); return identityService.newUserIdentity(subject, userPrincipal, new String[0]);
return null; return null;
} }
return new OpenIdUserIdentity(subject, userPrincipal, userIdentity); return new OpenIdUserIdentity(subject, userPrincipal, userIdentity);

View File

@ -23,7 +23,6 @@ import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.ConstraintMapping; import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler; import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
@ -35,8 +34,11 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
@SuppressWarnings("unchecked")
public class OpenIdAuthenticationTest public class OpenIdAuthenticationTest
{ {
public static final String CLIENT_ID = "testClient101"; public static final String CLIENT_ID = "testClient101";
@ -88,24 +90,22 @@ public class OpenIdAuthenticationTest
// security handler // security handler
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler(); ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
securityHandler.setRealmName("OpenID Connect Authentication"); assertThat(securityHandler.getKnownAuthenticatorFactories().size(), greaterThanOrEqualTo(2));
securityHandler.setAuthMethod(Constraint.__OPENID_AUTH);
securityHandler.setRealmName(openIdProvider.getProvider());
securityHandler.addConstraintMapping(profileMapping); securityHandler.addConstraintMapping(profileMapping);
securityHandler.addConstraintMapping(loginMapping); securityHandler.addConstraintMapping(loginMapping);
securityHandler.addConstraintMapping(adminMapping); securityHandler.addConstraintMapping(adminMapping);
// Authentication using local OIDC Provider // Authentication using local OIDC Provider
OpenIdConfiguration configuration = new OpenIdConfiguration(openIdProvider.getProvider(), CLIENT_ID, CLIENT_SECRET); server.addBean(new OpenIdConfiguration(openIdProvider.getProvider(), CLIENT_ID, CLIENT_SECRET));
securityHandler.setInitParameter(OpenIdAuthenticator.REDIRECT_PATH, "/redirect_path");
// Configure OpenIdLoginService optionally providing a base LoginService to provide user roles securityHandler.setInitParameter(OpenIdAuthenticator.ERROR_PAGE, "/error");
OpenIdLoginService loginService = new OpenIdLoginService(configuration);
securityHandler.setLoginService(loginService);
Authenticator authenticator = new OpenIdAuthenticator(configuration, "/error");
securityHandler.setAuthenticator(authenticator);
context.setSecurityHandler(securityHandler); context.setSecurityHandler(securityHandler);
server.start(); server.start();
String redirectUri = "http://localhost:" + connector.getLocalPort() + "/j_security_check"; String redirectUri = "http://localhost:" + connector.getLocalPort() + "/redirect_path";
openIdProvider.addRedirectUri(redirectUri); openIdProvider.addRedirectUri(redirectUri);
client = new HttpClient(); client = new HttpClient();
@ -122,30 +122,29 @@ public class OpenIdAuthenticationTest
@Test @Test
public void testLoginLogout() throws Exception public void testLoginLogout() throws Exception
{ {
openIdProvider.setUser(new OpenIdProvider.User("123456789", "Alice"));
String appUriString = "http://localhost:" + connector.getLocalPort(); String appUriString = "http://localhost:" + connector.getLocalPort();
// Initially not authenticated // Initially not authenticated
ContentResponse response = client.GET(appUriString + "/"); ContentResponse response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.OK_200)); assertThat(response.getStatus(), is(HttpStatus.OK_200));
String[] content = response.getContentAsString().split("[\r\n]+"); String content = response.getContentAsString();
assertThat(content.length, is(1)); assertThat(content, containsString("not authenticated"));
assertThat(content[0], is("not authenticated"));
// Request to login is success // Request to login is success
response = client.GET(appUriString + "/login"); response = client.GET(appUriString + "/login");
assertThat(response.getStatus(), is(HttpStatus.OK_200)); assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString().split("[\r\n]+"); content = response.getContentAsString();
assertThat(content.length, is(1)); assertThat(content, containsString("success"));
assertThat(content[0], is("success"));
// Now authenticated we can get info // Now authenticated we can get info
response = client.GET(appUriString + "/"); response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.OK_200)); assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString().split("[\r\n]+"); content = response.getContentAsString();
assertThat(content.length, is(3)); assertThat(content, containsString("userId: 123456789"));
assertThat(content[0], is("userId: 123456789")); assertThat(content, containsString("name: Alice"));
assertThat(content[1], is("name: Alice")); assertThat(content, containsString("email: Alice@example.com"));
assertThat(content[2], is("email: Alice@example.com"));
// Request to admin page gives 403 as we do not have admin role // Request to admin page gives 403 as we do not have admin role
response = client.GET(appUriString + "/admin"); response = client.GET(appUriString + "/admin");
@ -154,9 +153,8 @@ public class OpenIdAuthenticationTest
// We are no longer authenticated after logging out // We are no longer authenticated after logging out
response = client.GET(appUriString + "/logout"); response = client.GET(appUriString + "/logout");
assertThat(response.getStatus(), is(HttpStatus.OK_200)); assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString().split("[\r\n]+"); content = response.getContentAsString();
assertThat(content.length, is(1)); assertThat(content, containsString("not authenticated"));
assertThat(content[0], is("not authenticated"));
} }
public static class LoginPage extends HttpServlet public static class LoginPage extends HttpServlet
@ -164,7 +162,9 @@ public class OpenIdAuthenticationTest
@Override @Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{ {
response.setContentType("text/html");
response.getWriter().println("success"); response.getWriter().println("success");
response.getWriter().println("<br><a href=\"/\">Home</a>");
} }
} }
@ -183,7 +183,7 @@ public class OpenIdAuthenticationTest
@Override @Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{ {
Map<String, Object> userInfo = (Map)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS); Map<String, Object> userInfo = (Map<String, Object>)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
response.getWriter().println(userInfo.get("sub") + ": success"); response.getWriter().println(userInfo.get("sub") + ": success");
} }
} }
@ -193,18 +193,20 @@ public class OpenIdAuthenticationTest
@Override @Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{ {
response.setContentType("text/plain"); response.setContentType("text/html");
Principal userPrincipal = request.getUserPrincipal(); Principal userPrincipal = request.getUserPrincipal();
if (userPrincipal != null) if (userPrincipal != null)
{ {
Map<String, Object> userInfo = (Map)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS); Map<String, Object> userInfo = (Map<String, Object>)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
response.getWriter().println("userId: " + userInfo.get("sub")); response.getWriter().println("userId: " + userInfo.get("sub") + "<br>");
response.getWriter().println("name: " + userInfo.get("name")); response.getWriter().println("name: " + userInfo.get("name") + "<br>");
response.getWriter().println("email: " + userInfo.get("email")); response.getWriter().println("email: " + userInfo.get("email") + "<br>");
response.getWriter().println("<br><a href=\"/logout\">Logout</a>");
} }
else else
{ {
response.getWriter().println("not authenticated"); response.getWriter().println("not authenticated");
response.getWriter().println("<br><a href=\"/login\">Login</a>");
} }
} }
} }
@ -214,8 +216,9 @@ public class OpenIdAuthenticationTest
@Override @Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{ {
response.setContentType("text/plain"); response.setContentType("text/html");
response.getWriter().println("not authorized"); response.getWriter().println("not authorized");
response.getWriter().println("<br><a href=\"/\">Home</a>");
} }
} }
} }

View File

@ -14,6 +14,7 @@
package org.eclipse.jetty.security.openid; package org.eclipse.jetty.security.openid;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -21,7 +22,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
@ -37,9 +38,13 @@ import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OpenIdProvider extends ContainerLifeCycle public class OpenIdProvider extends ContainerLifeCycle
{ {
private static final Logger LOG = LoggerFactory.getLogger(OpenIdProvider.class);
private static final String CONFIG_PATH = "/.well-known/openid-configuration"; private static final String CONFIG_PATH = "/.well-known/openid-configuration";
private static final String AUTH_PATH = "/auth"; private static final String AUTH_PATH = "/auth";
private static final String TOKEN_PATH = "/token"; private static final String TOKEN_PATH = "/token";
@ -48,10 +53,32 @@ public class OpenIdProvider extends ContainerLifeCycle
protected final String clientId; protected final String clientId;
protected final String clientSecret; protected final String clientSecret;
protected final List<String> redirectUris = new ArrayList<>(); protected final List<String> redirectUris = new ArrayList<>();
private final ServerConnector connector;
private final Server server;
private int port = 0;
private String provider; private String provider;
private Server server; private User preAuthedUser;
private ServerConnector connector;
public static void main(String[] args) throws Exception
{
String clientId = "CLIENT_ID123";
String clientSecret = "PASSWORD123";
int port = 5771;
String redirectUri = "http://localhost:8080/openid/auth";
OpenIdProvider openIdProvider = new OpenIdProvider(clientId, clientSecret);
openIdProvider.addRedirectUri(redirectUri);
openIdProvider.setPort(port);
openIdProvider.start();
try
{
openIdProvider.join();
}
finally
{
openIdProvider.stop();
}
}
public OpenIdProvider(String clientId, String clientSecret) public OpenIdProvider(String clientId, String clientSecret)
{ {
@ -72,17 +99,43 @@ public class OpenIdProvider extends ContainerLifeCycle
addBean(server); addBean(server);
} }
public void join() throws InterruptedException
{
server.join();
}
public OpenIdConfiguration getOpenIdConfiguration()
{
String provider = getProvider();
String authEndpoint = provider + AUTH_PATH;
String tokenEndpoint = provider + TOKEN_PATH;
return new OpenIdConfiguration(provider, authEndpoint, tokenEndpoint, clientId, clientSecret, null);
}
@Override @Override
protected void doStart() throws Exception protected void doStart() throws Exception
{ {
connector.setPort(port);
super.doStart(); super.doStart();
provider = "http://localhost:" + connector.getLocalPort(); provider = "http://localhost:" + connector.getLocalPort();
} }
public void setPort(int port)
{
if (isStarted())
throw new IllegalStateException();
this.port = port;
}
public void setUser(User user)
{
this.preAuthedUser = user;
}
public String getProvider() public String getProvider()
{ {
if (!isStarted()) if (!isStarted() && port == 0)
throw new IllegalStateException(); throw new IllegalStateException("Port of OpenIdProvider not configured");
return provider; return provider;
} }
@ -94,7 +147,7 @@ public class OpenIdProvider extends ContainerLifeCycle
public class OpenIdAuthEndpoint extends HttpServlet public class OpenIdAuthEndpoint extends HttpServlet
{ {
@Override @Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
{ {
if (!clientId.equals(req.getParameter("client_id"))) if (!clientId.equals(req.getParameter("client_id")))
{ {
@ -105,6 +158,7 @@ public class OpenIdProvider extends ContainerLifeCycle
String redirectUri = req.getParameter("redirect_uri"); String redirectUri = req.getParameter("redirect_uri");
if (!redirectUris.contains(redirectUri)) if (!redirectUris.contains(redirectUri))
{ {
LOG.warn("invalid redirectUri {}", redirectUri);
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri"); resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri");
return; return;
} }
@ -130,16 +184,71 @@ public class OpenIdProvider extends ContainerLifeCycle
return; return;
} }
if (preAuthedUser == null)
{
PrintWriter writer = resp.getWriter();
resp.setContentType("text/html");
writer.println("<h2>Login to OpenID Connect Provider</h2>");
writer.println("<form action=\"" + AUTH_PATH + "\" method=\"post\">");
writer.println("<input type=\"text\" autocomplete=\"off\" placeholder=\"Username\" name=\"username\" required>");
writer.println("<input type=\"hidden\" name=\"redirectUri\" value=\"" + redirectUri + "\">");
writer.println("<input type=\"hidden\" name=\"state\" value=\"" + state + "\">");
writer.println("<input type=\"submit\">");
writer.println("</form>");
}
else
{
redirectUser(req, preAuthedUser, redirectUri, state);
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException
{
String redirectUri = req.getParameter("redirectUri");
if (!redirectUris.contains(redirectUri))
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri");
return;
}
String state = req.getParameter("state");
if (state == null)
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param");
return;
}
String username = req.getParameter("username");
if (username == null)
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no username");
return;
}
User user = new User(username);
redirectUser(req, user, redirectUri, state);
}
public void redirectUser(HttpServletRequest request, User user, String redirectUri, String state) throws IOException
{
String authCode = UUID.randomUUID().toString().replace("-", ""); String authCode = UUID.randomUUID().toString().replace("-", "");
User user = new User(123456789, "Alice");
issuedAuthCodes.put(authCode, user); issuedAuthCodes.put(authCode, user);
final Request baseRequest = Request.getBaseRequest(req); try
final Response baseResponse = baseRequest.getResponse(); {
redirectUri += "?code=" + authCode + "&state=" + state; final Request baseRequest = Objects.requireNonNull(Request.getBaseRequest(request));
int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() final Response baseResponse = baseRequest.getResponse();
? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER); redirectUri += "?code=" + authCode + "&state=" + state;
baseResponse.sendRedirect(redirectCode, resp.encodeRedirectURL(redirectUri)); int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion()
? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
baseResponse.sendRedirect(redirectCode, baseResponse.encodeRedirectURL(redirectUri));
}
catch (Throwable t)
{
issuedAuthCodes.remove(authCode);
throw t;
}
} }
} }
@ -171,7 +280,7 @@ public class OpenIdProvider extends ContainerLifeCycle
long expiry = System.currentTimeMillis() + Duration.ofMinutes(10).toMillis(); long expiry = System.currentTimeMillis() + Duration.ofMinutes(10).toMillis();
String response = "{" + String response = "{" +
"\"access_token\": \"" + accessToken + "\"," + "\"access_token\": \"" + accessToken + "\"," +
"\"id_token\": \"" + JwtEncoder.encode(user.getIdToken()) + "\"," + "\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId)) + "\"," +
"\"expires_in\": " + expiry + "," + "\"expires_in\": " + expiry + "," +
"\"token_type\": \"Bearer\"" + "\"token_type\": \"Bearer\"" +
"}"; "}";
@ -184,7 +293,7 @@ public class OpenIdProvider extends ContainerLifeCycle
public class OpenIdConfigServlet extends HttpServlet public class OpenIdConfigServlet extends HttpServlet
{ {
@Override @Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
{ {
String discoveryDocument = "{" + String discoveryDocument = "{" +
"\"issuer\": \"" + provider + "\"," + "\"issuer\": \"" + provider + "\"," +
@ -196,17 +305,17 @@ public class OpenIdProvider extends ContainerLifeCycle
} }
} }
public class User public static class User
{ {
private long subject; private final String subject;
private String name; private final String name;
public User(String name) public User(String name)
{ {
this(new Random().nextLong(), name); this(UUID.nameUUIDFromBytes(name.getBytes()).toString(), name);
} }
public User(long subject, String name) public User(String subject, String name)
{ {
this.subject = subject; this.subject = subject;
this.name = name; this.name = name;
@ -217,10 +326,15 @@ public class OpenIdProvider extends ContainerLifeCycle
return name; return name;
} }
public String getIdToken() public String getSubject()
{
return subject;
}
public String getIdToken(String provider, String clientId)
{ {
long expiry = System.currentTimeMillis() + Duration.ofMinutes(1).toMillis(); long expiry = System.currentTimeMillis() + Duration.ofMinutes(1).toMillis();
return JwtEncoder.createIdToken(provider, clientId, Long.toString(subject), name, expiry); return JwtEncoder.createIdToken(provider, clientId, subject, name, expiry);
} }
} }
} }

View File

@ -0,0 +1,184 @@
//
// ========================================================================
// Copyright (c) 1995-2021 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.openid;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.security.Constraint;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class OpenIdReamNameTest
{
private final Server server = new Server();
public static ServletContextHandler configureOpenIdContext(String realmName)
{
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
assertThat(securityHandler.getKnownAuthenticatorFactories().size(), greaterThanOrEqualTo(2));
securityHandler.setAuthMethod(Constraint.__OPENID_AUTH);
securityHandler.setRealmName(realmName);
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/" + realmName);
context.setSecurityHandler(securityHandler);
return context;
}
@Test
public void testSingleConfiguration() throws Exception
{
// Add some OpenID configurations.
OpenIdConfiguration config1 = new OpenIdConfiguration("provider1",
"", "", "", "", null);
server.addBean(config1);
// Configure two webapps to select configs based on realm name.
ServletContextHandler context1 = configureOpenIdContext("This doesn't matter if only 1 OpenIdConfiguration");
ContextHandlerCollection contextHandlerCollection = new ContextHandlerCollection();
contextHandlerCollection.addHandler(context1);
server.setHandler(contextHandlerCollection);
try
{
server.start();
// The OpenIdConfiguration from context1 matches to config1.
Authenticator authenticator = context1.getSecurityHandler().getAuthenticator();
assertThat(authenticator, instanceOf(OpenIdAuthenticator.class));
LoginService loginService = ((OpenIdAuthenticator)authenticator).getLoginService();
assertThat(loginService, instanceOf(OpenIdLoginService.class));
assertThat(((OpenIdLoginService)loginService).getConfiguration(), is(config1));
}
finally
{
server.stop();
}
}
@Test
public void testSingleConfigurationNoRealmName() throws Exception
{
// Add some OpenID configurations.
OpenIdConfiguration config1 = new OpenIdConfiguration("provider1",
"", "", "", "", null);
server.addBean(config1);
// Configure two webapps to select configs based on realm name.
ServletContextHandler context1 = configureOpenIdContext(null);
ContextHandlerCollection contextHandlerCollection = new ContextHandlerCollection();
contextHandlerCollection.addHandler(context1);
server.setHandler(contextHandlerCollection);
try
{
server.start();
// The OpenIdConfiguration from context1 matches to config1.
Authenticator authenticator = context1.getSecurityHandler().getAuthenticator();
assertThat(authenticator, instanceOf(OpenIdAuthenticator.class));
LoginService loginService = ((OpenIdAuthenticator)authenticator).getLoginService();
assertThat(loginService, instanceOf(OpenIdLoginService.class));
assertThat(((OpenIdLoginService)loginService).getConfiguration(), is(config1));
}
finally
{
server.stop();
}
}
@Test
public void testMultipleConfiguration() throws Exception
{
// Add some OpenID configurations.
OpenIdConfiguration config1 = new OpenIdConfiguration("provider1",
"", "", "", "", null);
OpenIdConfiguration config2 = new OpenIdConfiguration("provider2",
"", "", "", "", null);
server.addBean(config1);
server.addBean(config2);
// Configure two webapps to select configs based on realm name.
ServletContextHandler context1 = configureOpenIdContext(config1.getIssuer());
ServletContextHandler context2 = configureOpenIdContext(config2.getIssuer());
ContextHandlerCollection contextHandlerCollection = new ContextHandlerCollection();
contextHandlerCollection.addHandler(context1);
contextHandlerCollection.addHandler(context2);
server.setHandler(contextHandlerCollection);
try
{
server.start();
// The OpenIdConfiguration from context1 matches to config1.
Authenticator authenticator = context1.getSecurityHandler().getAuthenticator();
assertThat(authenticator, instanceOf(OpenIdAuthenticator.class));
LoginService loginService = ((OpenIdAuthenticator)authenticator).getLoginService();
assertThat(loginService, instanceOf(OpenIdLoginService.class));
assertThat(((OpenIdLoginService)loginService).getConfiguration(), is(config1));
// The OpenIdConfiguration from context2 matches to config2.
authenticator = context2.getSecurityHandler().getAuthenticator();
assertThat(authenticator, instanceOf(OpenIdAuthenticator.class));
loginService = ((OpenIdAuthenticator)authenticator).getLoginService();
assertThat(loginService, instanceOf(OpenIdLoginService.class));
assertThat(((OpenIdLoginService)loginService).getConfiguration(), is(config2));
}
finally
{
server.stop();
}
}
@Test
public void testMultipleConfigurationNoMatch() throws Exception
{
// Add some OpenID configurations.
OpenIdConfiguration config1 = new OpenIdConfiguration("provider1",
"", "", "", "", null);
OpenIdConfiguration config2 = new OpenIdConfiguration("provider2",
"", "", "", "", null);
server.addBean(config1);
server.addBean(config2);
// Configure two webapps to select configs based on realm name.
ServletContextHandler context1 = configureOpenIdContext("provider3");
ContextHandlerCollection contextHandlerCollection = new ContextHandlerCollection();
contextHandlerCollection.addHandler(context1);
server.setHandler(contextHandlerCollection);
// Multiple OpenIdConfigurations were available and didn't match one based on realm name.
assertThrows(IllegalStateException.class, server::start);
}
@Test
public void testNoConfiguration() throws Exception
{
ServletContextHandler context1 = configureOpenIdContext(null);
ContextHandlerCollection contextHandlerCollection = new ContextHandlerCollection();
contextHandlerCollection.addHandler(context1);
server.setHandler(contextHandlerCollection);
// If no OpenIdConfigurations are present it is bad configuration.
assertThrows(IllegalStateException.class, server::start);
}
}