Issue #7042 - Allow OpenIdConfiguration to be selected based on realm name.
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
parent
2426b34a51
commit
b8af57153a
|
@ -20,30 +20,29 @@
|
|||
</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><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>
|
||||
<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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
import org.eclipse.jetty.security.LoginService;
|
||||
import org.eclipse.jetty.security.WrappedAuthConfiguration;
|
||||
|
||||
public class OpenIdAuthConfiguration extends WrappedAuthConfiguration
|
||||
{
|
||||
public static final String AUTHENTICATE_NEW_USERS_INIT_PARAM = "jetty.openid.authenticateNewUsers";
|
||||
|
||||
private final OpenIdLoginService _openIdLoginService;
|
||||
|
||||
public OpenIdAuthConfiguration(OpenIdConfiguration openIdConfiguration, Authenticator.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());
|
||||
|
||||
String authNewUsers = authConfiguration.getInitParameter(AUTHENTICATE_NEW_USERS_INIT_PARAM);
|
||||
if (authNewUsers != null)
|
||||
_openIdLoginService.setAuthenticateNewUsers(Boolean.parseBoolean(authNewUsers));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginService getLoginService()
|
||||
{
|
||||
return _openIdLoginService;
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
@ -79,6 +81,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
|
|||
|
||||
private final SecureRandom _secureRandom = new SecureRandom();
|
||||
private OpenIdConfiguration _configuration;
|
||||
private String _redirectPath;
|
||||
private String _errorPage;
|
||||
private String _errorPath;
|
||||
private String _errorQuery;
|
||||
|
@ -86,11 +89,23 @@ 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)
|
||||
{
|
||||
_configuration = configuration;
|
||||
setRedirectPath(redirectPath);
|
||||
if (errorPage != null)
|
||||
setErrorPage(errorPage);
|
||||
}
|
||||
|
@ -98,19 +113,23 @@ public class OpenIdAuthenticator extends LoginAuthenticator
|
|||
@Override
|
||||
public void setConfiguration(AuthConfiguration configuration)
|
||||
{
|
||||
super.setConfiguration(configuration);
|
||||
if (_configuration == null)
|
||||
{
|
||||
LoginService loginService = configuration.getLoginService();
|
||||
if (!(loginService instanceof OpenIdLoginService))
|
||||
throw new IllegalArgumentException("invalid LoginService " + loginService);
|
||||
this._configuration = ((OpenIdLoginService)loginService).getConfiguration();
|
||||
}
|
||||
|
||||
String redirectPath = configuration.getInitParameter(REDIRECT_PATH);
|
||||
if (redirectPath != null)
|
||||
_redirectPath = redirectPath;
|
||||
|
||||
String error = configuration.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(_configuration, configuration));
|
||||
}
|
||||
|
||||
@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, _configuration.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();
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,20 @@ public class OpenIdAuthenticatorFactory implements Authenticator.Factory
|
|||
{
|
||||
String auth = configuration.getAuthMethod();
|
||||
if (Constraint.__OPENID_AUTH.equalsIgnoreCase(auth))
|
||||
return new OpenIdAuthenticator();
|
||||
{
|
||||
// If LoginService is an OpenIdLoginService it already contains the configuration and will be obtained in setConfiguration();
|
||||
if (loginService instanceof OpenIdLoginService)
|
||||
return new OpenIdAuthenticator();
|
||||
|
||||
// Otherwise we should find an OpenIdConfiguration for this realm on the Server.
|
||||
String realmName = configuration.getRealmName();
|
||||
OpenIdConfiguration openIdConfiguration = server.getBeans(OpenIdConfiguration.class).stream()
|
||||
.filter(c -> c.getIssuer().equals(realmName))
|
||||
.findAny()
|
||||
.orElseThrow(() -> new IllegalStateException("No OpenIdConfiguration found for realm \"" + realmName + "\""));
|
||||
return new OpenIdAuthenticator(openIdConfiguration);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue