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>
<Set name="executor"><Ref refid="ThreadPool"/></Set>
</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">
<Arg>
<New class="org.eclipse.jetty.security.openid.OpenIdLoginService">
<Arg><Ref refid="OpenIdConfiguration"/></Arg>
<Arg><Ref refid="BaseLoginService"/></Arg>
<Call name="setAuthenticateNewUsers">
<Arg type="boolean">
<Property name="jetty.openid.authenticateNewUsers" default="false"/>
<Ref refid="BaseLoginService"/>
</Arg>
</Call>
<Call name="addBean">
<Arg>
<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>
</Call>
</New>
</Arg>
</Call>
</Configure>
</Configure>

View File

@ -20,7 +20,7 @@ etc/jetty-openid.xml
[ini-template]
## 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)
# 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
{
requires org.eclipse.jetty.util.ajax;
@ -19,4 +21,6 @@ module org.eclipse.jetty.security.openid
requires transitive org.eclipse.jetty.security;
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>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}.
* </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 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 J_URI = "org.eclipse.jetty.security.openid.URI";
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";
private final SecureRandom _secureRandom = new SecureRandom();
private OpenIdConfiguration _configuration;
private OpenIdConfiguration _openIdConfiguration;
private String _redirectPath;
private String _errorPage;
private String _errorPath;
private String _errorQuery;
@ -86,31 +89,47 @@ public class OpenIdAuthenticator extends LoginAuthenticator
public OpenIdAuthenticator()
{
this(null, J_SECURITY_CHECK, null);
}
public OpenIdAuthenticator(OpenIdConfiguration configuration)
{
this(configuration, J_SECURITY_CHECK, null);
}
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)
setErrorPage(errorPage);
}
@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)
setErrorPage(error);
if (_configuration != null)
return;
LoginService loginService = configuration.getLoginService();
if (!(loginService instanceof OpenIdLoginService))
throw new IllegalArgumentException("invalid LoginService");
this._configuration = ((OpenIdLoginService)loginService).getConfiguration();
super.setConfiguration(new OpenIdAuthConfiguration(_openIdConfiguration, authConfig));
}
@Override
@ -131,7 +150,23 @@ public class OpenIdAuthenticator extends LoginAuthenticator
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)
{
@ -174,6 +209,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
session.setAttribute(SessionAuthentication.__J_AUTHENTICATED, cached);
session.setAttribute(CLAIMS, ((OpenIdCredentials)credentials).getClaims());
session.setAttribute(RESPONSE, ((OpenIdCredentials)credentials).getResponse());
session.setAttribute(ISSUER, _openIdConfiguration.getIssuer());
}
}
return user;
@ -445,11 +481,11 @@ public class OpenIdAuthenticator extends LoginAuthenticator
public boolean isJSecurityCheck(String uri)
{
int jsc = uri.indexOf(J_SECURITY_CHECK);
int jsc = uri.indexOf(_redirectPath);
if (jsc < 0)
return false;
int e = jsc + J_SECURITY_CHECK.length();
int e = jsc + _redirectPath.length();
if (e == uri.length())
return true;
char c = uri.charAt(e);
@ -467,7 +503,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
URIUtil.appendSchemeHostPort(redirectUri, request.getScheme(),
request.getServerName(), request.getServerPort());
redirectUri.append(request.getContextPath());
redirectUri.append(J_SECURITY_CHECK);
redirectUri.append(_redirectPath);
return redirectUri.toString();
}
@ -484,13 +520,13 @@ public class OpenIdAuthenticator extends LoginAuthenticator
// any custom scopes requested from configuration
StringBuilder scopes = new StringBuilder();
for (String s : _configuration.getScopes())
for (String s : _openIdConfiguration.getScopes())
{
scopes.append(" ").append(s);
}
return _configuration.getAuthEndpoint() +
"?client_id=" + UrlEncoded.encodeString(_configuration.getClientId(), StandardCharsets.UTF_8) +
return _openIdConfiguration.getAuthEndpoint() +
"?client_id=" + UrlEncoded.encodeString(_openIdConfiguration.getClientId(), StandardCharsets.UTF_8) +
"&redirect_uri=" + UrlEncoded.encodeString(getRedirectUri(request), StandardCharsets.UTF_8) +
"&scope=openid" + UrlEncoded.encodeString(scopes.toString(), StandardCharsets.UTF_8) +
"&state=" + antiForgeryToken +

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.security.openid;
import java.util.Collection;
import javax.servlet.ServletContext;
import org.eclipse.jetty.security.Authenticator;
@ -28,7 +29,29 @@ public class OpenIdAuthenticatorFactory implements Authenticator.Factory
{
String auth = configuration.getAuthMethod();
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;
}
}

View File

@ -48,6 +48,7 @@ public class OpenIdConfiguration extends ContainerLifeCycle
private final String authMethod;
private String authEndpoint;
private String tokenEndpoint;
private boolean authenticateNewUsers = false;
/**
* 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);
Map<String, Object> result;
String responseBody = httpClient.GET(provider + CONFIG_PATH)
.getContentAsString();
String responseBody = httpClient.GET(provider + CONFIG_PATH).getContentAsString();
Object parsedResult = new JSON().fromJSON(responseBody);
if (parsedResult instanceof Map)
{
Map<?, ?> rawResult = (Map<?, ?>)parsedResult;
result = rawResult.entrySet().stream()
.filter(entry -> entry.getValue() != null)
.collect(Collectors.toMap(it -> it.getKey().toString(), Map.Entry::getValue));
if (LOG.isDebugEnabled())
LOG.debug("discovery document {}", result);
return result;
}
else
{
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");
}
LOG.debug("discovery document {}", result);
return result;
}
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;
}
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;
import java.util.Objects;
import javax.security.auth.Subject;
import javax.servlet.ServletRequest;
@ -53,10 +54,12 @@ public class OpenIdLoginService extends ContainerLifeCycle implements LoginServi
*/
public OpenIdLoginService(OpenIdConfiguration configuration, LoginService loginService)
{
this.configuration = configuration;
this.configuration = Objects.requireNonNull(configuration);
this.loginService = loginService;
addBean(this.configuration);
addBean(this.loginService);
setAuthenticateNewUsers(configuration.isAuthenticateNewUsers());
}
@Override
@ -93,13 +96,14 @@ public class OpenIdLoginService extends ContainerLifeCycle implements LoginServi
subject.getPrivateCredentials().add(credentials);
subject.setReadOnly();
IdentityService identityService = getIdentityService();
if (loginService != null)
{
UserIdentity userIdentity = loginService.login(openIdCredentials.getUserId(), "", req);
if (userIdentity == null)
{
if (isAuthenticateNewUsers())
return getIdentityService().newUserIdentity(subject, userPrincipal, new String[0]);
return identityService.newUserIdentity(subject, userPrincipal, new String[0]);
return null;
}
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.api.ContentResponse;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.server.Server;
@ -35,8 +34,11 @@ 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.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;
@SuppressWarnings("unchecked")
public class OpenIdAuthenticationTest
{
public static final String CLIENT_ID = "testClient101";
@ -88,24 +90,22 @@ public class OpenIdAuthenticationTest
// security handler
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(loginMapping);
securityHandler.addConstraintMapping(adminMapping);
// Authentication using local OIDC Provider
OpenIdConfiguration configuration = new OpenIdConfiguration(openIdProvider.getProvider(), CLIENT_ID, CLIENT_SECRET);
// Configure OpenIdLoginService optionally providing a base LoginService to provide user roles
OpenIdLoginService loginService = new OpenIdLoginService(configuration);
securityHandler.setLoginService(loginService);
Authenticator authenticator = new OpenIdAuthenticator(configuration, "/error");
securityHandler.setAuthenticator(authenticator);
server.addBean(new OpenIdConfiguration(openIdProvider.getProvider(), CLIENT_ID, CLIENT_SECRET));
securityHandler.setInitParameter(OpenIdAuthenticator.REDIRECT_PATH, "/redirect_path");
securityHandler.setInitParameter(OpenIdAuthenticator.ERROR_PAGE, "/error");
context.setSecurityHandler(securityHandler);
server.start();
String redirectUri = "http://localhost:" + connector.getLocalPort() + "/j_security_check";
String redirectUri = "http://localhost:" + connector.getLocalPort() + "/redirect_path";
openIdProvider.addRedirectUri(redirectUri);
client = new HttpClient();
@ -122,30 +122,29 @@ public class OpenIdAuthenticationTest
@Test
public void testLoginLogout() throws Exception
{
openIdProvider.setUser(new OpenIdProvider.User("123456789", "Alice"));
String appUriString = "http://localhost:" + connector.getLocalPort();
// Initially not authenticated
ContentResponse response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
String[] content = response.getContentAsString().split("[\r\n]+");
assertThat(content.length, is(1));
assertThat(content[0], is("not authenticated"));
String content = response.getContentAsString();
assertThat(content, containsString("not authenticated"));
// Request to login is success
response = client.GET(appUriString + "/login");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString().split("[\r\n]+");
assertThat(content.length, is(1));
assertThat(content[0], is("success"));
content = response.getContentAsString();
assertThat(content, containsString("success"));
// Now authenticated we can get info
response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString().split("[\r\n]+");
assertThat(content.length, is(3));
assertThat(content[0], is("userId: 123456789"));
assertThat(content[1], is("name: Alice"));
assertThat(content[2], is("email: Alice@example.com"));
content = response.getContentAsString();
assertThat(content, containsString("userId: 123456789"));
assertThat(content, containsString("name: Alice"));
assertThat(content, containsString("email: Alice@example.com"));
// Request to admin page gives 403 as we do not have admin role
response = client.GET(appUriString + "/admin");
@ -154,9 +153,8 @@ public class OpenIdAuthenticationTest
// We are no longer authenticated after logging out
response = client.GET(appUriString + "/logout");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString().split("[\r\n]+");
assertThat(content.length, is(1));
assertThat(content[0], is("not authenticated"));
content = response.getContentAsString();
assertThat(content, containsString("not authenticated"));
}
public static class LoginPage extends HttpServlet
@ -164,7 +162,9 @@ public class OpenIdAuthenticationTest
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setContentType("text/html");
response.getWriter().println("success");
response.getWriter().println("<br><a href=\"/\">Home</a>");
}
}
@ -183,7 +183,7 @@ public class OpenIdAuthenticationTest
@Override
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");
}
}
@ -193,18 +193,20 @@ public class OpenIdAuthenticationTest
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setContentType("text/plain");
response.setContentType("text/html");
Principal userPrincipal = request.getUserPrincipal();
if (userPrincipal != null)
{
Map<String, Object> userInfo = (Map)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
response.getWriter().println("userId: " + userInfo.get("sub"));
response.getWriter().println("name: " + userInfo.get("name"));
response.getWriter().println("email: " + userInfo.get("email"));
Map<String, Object> userInfo = (Map<String, Object>)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
response.getWriter().println("userId: " + userInfo.get("sub") + "<br>");
response.getWriter().println("name: " + userInfo.get("name") + "<br>");
response.getWriter().println("email: " + userInfo.get("email") + "<br>");
response.getWriter().println("<br><a href=\"/logout\">Logout</a>");
}
else
{
response.getWriter().println("not authenticated");
response.getWriter().println("<br><a href=\"/login\">Login</a>");
}
}
}
@ -214,8 +216,9 @@ public class OpenIdAuthenticationTest
@Override
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("<br><a href=\"/\">Home</a>");
}
}
}
}

View File

@ -14,6 +14,7 @@
package org.eclipse.jetty.security.openid;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
@ -21,7 +22,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Objects;
import java.util.UUID;
import javax.servlet.ServletException;
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.util.StringUtil;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 AUTH_PATH = "/auth";
private static final String TOKEN_PATH = "/token";
@ -48,10 +53,32 @@ public class OpenIdProvider extends ContainerLifeCycle
protected final String clientId;
protected final String clientSecret;
protected final List<String> redirectUris = new ArrayList<>();
private final ServerConnector connector;
private final Server server;
private int port = 0;
private String provider;
private Server server;
private ServerConnector connector;
private User preAuthedUser;
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)
{
@ -72,17 +99,43 @@ public class OpenIdProvider extends ContainerLifeCycle
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
protected void doStart() throws Exception
{
connector.setPort(port);
super.doStart();
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()
{
if (!isStarted())
throw new IllegalStateException();
if (!isStarted() && port == 0)
throw new IllegalStateException("Port of OpenIdProvider not configured");
return provider;
}
@ -94,7 +147,7 @@ public class OpenIdProvider extends ContainerLifeCycle
public class OpenIdAuthEndpoint extends HttpServlet
{
@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")))
{
@ -105,6 +158,7 @@ public class OpenIdProvider extends ContainerLifeCycle
String redirectUri = req.getParameter("redirect_uri");
if (!redirectUris.contains(redirectUri))
{
LOG.warn("invalid redirectUri {}", redirectUri);
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri");
return;
}
@ -130,16 +184,71 @@ public class OpenIdProvider extends ContainerLifeCycle
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("-", "");
User user = new User(123456789, "Alice");
issuedAuthCodes.put(authCode, user);
final Request baseRequest = Request.getBaseRequest(req);
final Response baseResponse = baseRequest.getResponse();
redirectUri += "?code=" + authCode + "&state=" + state;
int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion()
? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
baseResponse.sendRedirect(redirectCode, resp.encodeRedirectURL(redirectUri));
try
{
final Request baseRequest = Objects.requireNonNull(Request.getBaseRequest(request));
final Response baseResponse = baseRequest.getResponse();
redirectUri += "?code=" + authCode + "&state=" + state;
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();
String response = "{" +
"\"access_token\": \"" + accessToken + "\"," +
"\"id_token\": \"" + JwtEncoder.encode(user.getIdToken()) + "\"," +
"\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId)) + "\"," +
"\"expires_in\": " + expiry + "," +
"\"token_type\": \"Bearer\"" +
"}";
@ -184,7 +293,7 @@ public class OpenIdProvider extends ContainerLifeCycle
public class OpenIdConfigServlet extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
{
String discoveryDocument = "{" +
"\"issuer\": \"" + provider + "\"," +
@ -196,17 +305,17 @@ public class OpenIdProvider extends ContainerLifeCycle
}
}
public class User
public static class User
{
private long subject;
private String name;
private final String subject;
private final 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.name = name;
@ -217,10 +326,15 @@ public class OpenIdProvider extends ContainerLifeCycle
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();
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);
}
}