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:
commit
d755e3a742
|
@ -20,30 +20,29 @@
|
||||||
</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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 +
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue