add OpenId module to support OpenId Connect authentication

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan Roberts 2019-08-29 07:39:19 +10:00
parent 2b72f08f1b
commit d33b96f411
17 changed files with 1429 additions and 1 deletions

View File

@ -299,6 +299,11 @@
<artifactId>jetty-security</artifactId>
<version>9.4.21-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-openid</artifactId>
<version>9.4.21-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>

View File

@ -711,6 +711,11 @@
<artifactId>jetty-alpn-openjdk8-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-openid</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-conscrypt-server</artifactId>

64
jetty-openid/pom.xml Normal file
View File

@ -0,0 +1,64 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
<version>9.4.21-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-openid</artifactId>
<name>Jetty :: OpenID</name>
<description>Jetty OpenID Connect infrastructure</description>
<url>http://www.eclipse.org/jetty</url>
<properties>
<bundle-symbolic-name>${project.groupId}.openid</bundle-symbolic-name>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>findbugs-maven-plugin</artifactId>
<configuration>
<onlyAnalyze>org.eclipse.jetty.security.openid.*</onlyAnalyze>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-security</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util-ajax</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>jetty-http-tools</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,24 @@
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://www.eclipse.org/jetty/configure_10_0.dtd">
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<New id="OpenIdConfiguration" class="org.eclipse.jetty.security.openid.OpenIdConfiguration">
<Arg><Property name="jetty.openid.identityProvider"/></Arg>
<Arg><Property name="jetty.openid.clientId"/></Arg>
<Arg><Property name="jetty.openid.clientSecret"/></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>
</New>
</Arg>
</Call>
</Configure>

View File

@ -0,0 +1,31 @@
DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
[description]
Adds OpenId Connect authentication.
[depend]
security
[lib]
lib/jetty-openid-${jetty.version}.jar
lib/jetty-util-ajax-${jetty.version}.jar
[files]
basehome:modules/openid/openid-baseloginservice.xml|etc/openid-baseloginservice.xml
[xml]
etc/openid-baseloginservice.xml
etc/jetty-openid.xml
[ini-template]
## Identity Provider
# jetty.openid.identityProvider=https://accounts.google.com/
## Client ID
# jetty.openid.clientId=1051168419525-5nl60mkugb77p9j194mrh287p1e0ahfi.apps.googleusercontent.com
## Client Secret
# jetty.openid.clientSecret=XT_MIsSv_aUCGollauCaJY8S
## Scopes
# jetty.openid.scopes=email,profile

View File

@ -0,0 +1,10 @@
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://www.eclipse.org/jetty/configure_10_0.dtd">
<Configure id="BaseLoginService">
<!-- Optional code to configure the base LoginService used by the OpenIdLoginService
<New id="BaseLoginService" class="org.eclipse.jetty.security.HashLoginService">
<Set name="config">/Users/Lachlan/webtide/jetty-base/etc/realm.properties</Set>
<Set name="hotReload">true</Set>
</New>
-->
</Configure>

View File

@ -0,0 +1,481 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.security.openid;
import java.io.IOException;
import java.math.BigInteger;
import java.security.SecureRandom;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.ServerAuthException;
import org.eclipse.jetty.security.UserAuthentication;
import org.eclipse.jetty.security.authentication.DeferredAuthentication;
import org.eclipse.jetty.security.authentication.LoginAuthenticator;
import org.eclipse.jetty.security.authentication.SessionAuthentication;
import org.eclipse.jetty.server.Authentication;
import org.eclipse.jetty.server.Authentication.User;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.security.Constraint;
/**
* OpenId Connect Authenticator.
*
* <p>This authenticator implements authentication using OpenId Connect on top of OAuth 2.0.
*
* <p>The authenticator redirects unauthenticated requests to the identity providers authorization endpoint
* which will eventually redirect back to the redirectUri with an authorization code which will be exchanged with
* the token_endpoint for an id_token. The request is then restored back to the original uri requested.
* {@link SessionAuthentication} is then used to wrap Authentication results so that they are associated with the session.</p>
*/
public class OpenIdAuthenticator extends LoginAuthenticator
{
private static final Logger LOG = Log.getLogger(OpenIdAuthenticator.class);
public static final String __USER_CLAIMS = "org.eclipse.jetty.security.openid.user_claims";
public static final String __RESPONSE_JSON = "org.eclipse.jetty.security.openid.response";
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";
public static final String __J_METHOD = "org.eclipse.jetty.security.openid.METHOD";
public static final String __CSRF_TOKEN = "org.eclipse.jetty.security.openid.csrf_token";
public static final String __J_SECURITY_CHECK = "/j_security_check";
private OpenIdConfiguration _configuration;
private String _errorPage;
private String _errorPath;
private boolean _alwaysSaveUri;
public OpenIdAuthenticator()
{
}
public OpenIdAuthenticator(OpenIdConfiguration configuration, String errorPage)
{
this._configuration = configuration;
if (errorPage != null)
setErrorPage(errorPage);
}
@Override
public void setConfiguration(AuthConfiguration configuration)
{
super.setConfiguration(configuration);
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();
}
@Override
public String getAuthMethod()
{
return Constraint.__OPENID_AUTH;
}
/**
* If true, uris that cause a redirect to a login page will always
* be remembered. If false, only the first uri that leads to a login
* page redirect is remembered.
* See https://bugs.eclipse.org/bugs/show_bug.cgi?id=379909
*
* @param alwaysSave true to always save the uri
*/
public void setAlwaysSaveUri(boolean alwaysSave)
{
_alwaysSaveUri = alwaysSave;
}
public boolean getAlwaysSaveUri()
{
return _alwaysSaveUri;
}
private void setErrorPage(String path)
{
if (path == null || path.trim().length() == 0)
{
_errorPath = null;
_errorPage = null;
}
else
{
if (!path.startsWith("/"))
{
LOG.warn("error-page must start with /");
path = "/" + path;
}
_errorPage = path;
_errorPath = path;
if (_errorPath.indexOf('?') > 0)
_errorPath = _errorPath.substring(0, _errorPath.indexOf('?'));
}
}
@Override
public UserIdentity login(String username, Object credentials, ServletRequest request)
{
if (LOG.isDebugEnabled())
LOG.debug("login {} {} {}", username, credentials, request);
UserIdentity user = super.login(username, credentials, request);
if (user != null)
{
HttpSession session = ((HttpServletRequest)request).getSession();
Authentication cached = new SessionAuthentication(getAuthMethod(), user, credentials);
session.setAttribute(SessionAuthentication.__J_AUTHENTICATED, cached);
session.setAttribute(__USER_CLAIMS, ((OpenIdCredentials)credentials).getClaims());
session.setAttribute(__RESPONSE_JSON, ((OpenIdCredentials)credentials).getResponse());
}
return user;
}
@Override
public void logout(ServletRequest request)
{
super.logout(request);
HttpServletRequest httpRequest = (HttpServletRequest)request;
HttpSession session = httpRequest.getSession(false);
if (session == null)
return;
//clean up session
session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED);
session.removeAttribute(__USER_CLAIMS);
session.removeAttribute(__RESPONSE_JSON);
}
@Override
public void prepareRequest(ServletRequest request)
{
//if this is a request resulting from a redirect after auth is complete
//(ie its from a redirect to the original request uri) then due to
//browser handling of 302 redirects, the method may not be the same as
//that of the original request. Replace the method and original post
//params (if it was a post).
//
//See Servlet Spec 3.1 sec 13.6.3
HttpServletRequest httpRequest = (HttpServletRequest)request;
HttpSession session = httpRequest.getSession(false);
if (session == null || session.getAttribute(SessionAuthentication.__J_AUTHENTICATED) == null)
return; //not authenticated yet
String juri = (String)session.getAttribute(__J_URI);
if (juri == null || juri.length() == 0)
return; //no original uri saved
String method = (String)session.getAttribute(__J_METHOD);
if (method == null || method.length() == 0)
return; //didn't save original request method
StringBuffer buf = httpRequest.getRequestURL();
if (httpRequest.getQueryString() != null)
buf.append("?").append(httpRequest.getQueryString());
if (!juri.equals(buf.toString()))
return; //this request is not for the same url as the original
//restore the original request's method on this request
if (LOG.isDebugEnabled())
LOG.debug("Restoring original method {} for {} with method {}", method, juri, httpRequest.getMethod());
Request baseRequest = Request.getBaseRequest(request);
baseRequest.setMethod(method);
}
@Override
public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException
{
final HttpServletRequest request = (HttpServletRequest)req;
final HttpServletResponse response = (HttpServletResponse)res;
final Request baseRequest = Request.getBaseRequest(request);
final Response baseResponse = baseRequest.getResponse();
String uri = request.getRequestURI();
if (uri == null)
uri = URIUtil.SLASH;
mandatory |= isJSecurityCheck(uri);
if (!mandatory)
return new DeferredAuthentication(this);
if (isErrorPage(URIUtil.addPaths(request.getServletPath(), request.getPathInfo())) && !DeferredAuthentication.isDeferred(response))
return new DeferredAuthentication(this);
try
{
// Handle a request for authentication.
if (isJSecurityCheck(uri))
{
String authCode = request.getParameter("code");
if (authCode != null)
{
// Verify anti-forgery state token
String state = request.getParameter("state");
String antiForgeryToken = (String)request.getSession().getAttribute(__CSRF_TOKEN);
if (antiForgeryToken == null || !antiForgeryToken.equals(state))
{
LOG.warn("auth failed 403: invalid state parameter");
if (response != null)
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return Authentication.SEND_FAILURE;
}
// Attempt to login with the provided authCode
OpenIdCredentials credentials = new OpenIdCredentials(authCode, getRedirectUri(request), _configuration);
UserIdentity user = login(null, credentials, request);
HttpSession session = request.getSession(false);
if (user != null)
{
// Redirect to original request
String nuri;
synchronized (session)
{
nuri = (String)session.getAttribute(__J_URI);
if (nuri == null || nuri.length() == 0)
{
nuri = request.getContextPath();
if (nuri.length() == 0)
nuri = URIUtil.SLASH;
}
}
OpenIdAuthentication openIdAuth = new OpenIdAuthentication(getAuthMethod(), user);
LOG.debug("authenticated {}->{}", openIdAuth, nuri);
response.setContentLength(0);
int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
baseResponse.sendRedirect(redirectCode, response.encodeRedirectURL(nuri));
return openIdAuth;
}
}
// not authenticated
if (LOG.isDebugEnabled())
LOG.debug("OpenId authentication FAILED");
if (_errorPage == null)
{
if (LOG.isDebugEnabled())
LOG.debug("auth failed 403");
if (response != null)
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("auth failed {}", _errorPage);
int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
baseResponse.sendRedirect(redirectCode, response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), _errorPage)));
}
return Authentication.SEND_FAILURE;
}
// Look for cached authentication
HttpSession session = request.getSession(false);
Authentication authentication = session == null ? null : (Authentication)session.getAttribute(SessionAuthentication.__J_AUTHENTICATED);
if (authentication != null)
{
// Has authentication been revoked?
if (authentication instanceof Authentication.User &&
_loginService != null &&
!_loginService.validate(((Authentication.User)authentication).getUserIdentity()))
{
LOG.debug("auth revoked {}", authentication);
session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED);
}
else
{
synchronized (session)
{
String jUri = (String)session.getAttribute(__J_URI);
if (jUri != null)
{
//check if the request is for the same url as the original and restore
//params if it was a post
LOG.debug("auth retry {}->{}", authentication, jUri);
StringBuffer buf = request.getRequestURL();
if (request.getQueryString() != null)
buf.append("?").append(request.getQueryString());
if (jUri.equals(buf.toString()))
{
MultiMap<String> jPost = (MultiMap<String>)session.getAttribute(__J_POST);
if (jPost != null)
{
LOG.debug("auth rePOST {}->{}", authentication, jUri);
baseRequest.setContentParameters(jPost);
}
session.removeAttribute(__J_URI);
session.removeAttribute(__J_METHOD);
session.removeAttribute(__J_POST);
}
}
}
LOG.debug("auth {}", authentication);
return authentication;
}
}
// if we can't send challenge
if (DeferredAuthentication.isDeferred(response))
{
LOG.debug("auth deferred {}", session == null ? null : session.getId());
return Authentication.UNAUTHENTICATED;
}
// remember the current URI
session = (session != null ? session : request.getSession(true));
synchronized (session)
{
// But only if it is not set already, or we save every uri that leads to a login redirect
if (session.getAttribute(__J_URI) == null || _alwaysSaveUri)
{
StringBuffer buf = request.getRequestURL();
if (request.getQueryString() != null)
buf.append("?").append(request.getQueryString());
session.setAttribute(__J_URI, buf.toString());
session.setAttribute(__J_METHOD, request.getMethod());
if (MimeTypes.Type.FORM_ENCODED.is(req.getContentType()) && HttpMethod.POST.is(request.getMethod()))
{
MultiMap<String> formParameters = new MultiMap<>();
baseRequest.extractFormParameters(formParameters);
session.setAttribute(__J_POST, formParameters);
}
}
}
// send the the challenge
String challengeUri = getChallengeUri(request);
LOG.debug("challenge {}->{}", session.getId(), challengeUri);
int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
baseResponse.sendRedirect(redirectCode, response.encodeRedirectURL(challengeUri));
return Authentication.SEND_CONTINUE;
}
catch (IOException e)
{
throw new ServerAuthException(e);
}
}
public boolean isJSecurityCheck(String uri)
{
int jsc = uri.indexOf(__J_SECURITY_CHECK);
if (jsc < 0)
return false;
int e = jsc + __J_SECURITY_CHECK.length();
if (e == uri.length())
return true;
char c = uri.charAt(e);
return c == ';' || c == '#' || c == '/' || c == '?';
}
public boolean isErrorPage(String pathInContext)
{
return pathInContext != null && (pathInContext.equals(_errorPath));
}
private String getRedirectUri(HttpServletRequest request)
{
final StringBuffer redirectUri = new StringBuffer(128);
URIUtil.appendSchemeHostPort(redirectUri, request.getScheme(),
request.getServerName(), request.getServerPort());
redirectUri.append(request.getContextPath());
redirectUri.append(__J_SECURITY_CHECK);
return redirectUri.toString();
}
protected String getChallengeUri(HttpServletRequest request)
{
HttpSession session = request.getSession();
String antiForgeryToken;
synchronized (session)
{
antiForgeryToken = (session.getAttribute(__CSRF_TOKEN) == null)
? new BigInteger(130, new SecureRandom()).toString(32)
: (String)session.getAttribute(__CSRF_TOKEN);
session.setAttribute(__CSRF_TOKEN, antiForgeryToken);
}
// any custom scopes requested from configuration
StringBuilder scopes = new StringBuilder();
for (String s : _configuration.getScopes())
{
scopes.append("%20" + s);
}
return _configuration.getAuthEndpoint() +
"?client_id=" + _configuration.getClientId() +
"&redirect_uri=" + getRedirectUri(request) +
"&scope=openid" + scopes +
"&state=" + antiForgeryToken +
"&response_type=code";
}
@Override
public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser)
{
return true;
}
/**
* This Authentication represents a just completed OpenId Connect authentication.
* Subsequent requests from the same user are authenticated by the presents
* of a {@link SessionAuthentication} instance in their session.
*/
public static class OpenIdAuthentication extends UserAuthentication implements Authentication.ResponseSent
{
public OpenIdAuthentication(String method, UserIdentity userIdentity)
{
super(method, userIdentity);
}
@Override
public String toString()
{
return "OpenId" + super.toString();
}
}
}

View File

@ -0,0 +1,40 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.security.openid;
import javax.servlet.ServletContext;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.DefaultAuthenticatorFactory;
import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.security.Constraint;
public class OpenIdAuthenticatorFactory extends DefaultAuthenticatorFactory
{
@Override
public Authenticator getAuthenticator(Server server, ServletContext context, Authenticator.AuthConfiguration configuration, IdentityService identityService, LoginService loginService)
{
String auth = configuration.getAuthMethod();
if (Constraint.__OPENID_AUTH.equalsIgnoreCase(auth))
return new OpenIdAuthenticator();
return super.getAuthenticator(server, context, configuration, identityService, loginService);
}
}

View File

@ -0,0 +1,118 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.security.openid;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.ajax.JSON;
public class OpenIdConfiguration
{
private static String CONFIG_PATH = "/.well-known/openid-configuration";
private final String identityProvider;
private final String authEndpoint;
private final String tokenEndpoint;
private final String clientId;
private final String clientSecret;
private final Map<String, Object> discoveryDocument;
private List<String> scopes = new ArrayList<>();
public OpenIdConfiguration(String provider, String clientId, String clientSecret)
{
this.identityProvider = provider;
this.clientId = clientId;
this.clientSecret = clientSecret;
try
{
if (provider.endsWith("/"))
provider = provider.substring(0, provider.length() - 1);
URI providerUri = URI.create(provider + CONFIG_PATH);
InputStream inputStream = providerUri.toURL().openConnection().getInputStream();
String content = IO.toString(inputStream);
discoveryDocument = (Map)JSON.parse(content);
}
catch (Throwable e)
{
throw new IllegalArgumentException("invalid identity provider", e);
}
if (discoveryDocument.get("issuer") == null)
throw new IllegalArgumentException();
authEndpoint = (String)discoveryDocument.get("authorization_endpoint");
if (authEndpoint == null)
throw new IllegalArgumentException("authorization_endpoint");
tokenEndpoint = (String)discoveryDocument.get("token_endpoint");
if (tokenEndpoint == null)
throw new IllegalArgumentException("token_endpoint");
}
public Map<String, Object> getDiscoveryDocument()
{
return discoveryDocument;
}
public String getAuthEndpoint()
{
return authEndpoint;
}
public String getClientId()
{
return clientId;
}
public String getClientSecret()
{
return clientSecret;
}
public String getIdentityProvider()
{
return identityProvider;
}
public String getTokenEndpoint()
{
return tokenEndpoint;
}
public void addScopes(String... scopes)
{
for (String scope : scopes)
{
this.scopes.add(scope);
}
}
public List<String> getScopes()
{
return scopes;
}
}

View File

@ -0,0 +1,181 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.security.openid;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.ajax.JSON;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
public class OpenIdCredentials
{
private static final Logger LOG = Log.getLogger(OpenIdCredentials.class);
private final String redirectUri;
private final OpenIdConfiguration configuration;
private String authCode;
private Map<String, Object> response;
private Map<String, Object> claims;
public OpenIdCredentials(String authCode, String redirectUri, OpenIdConfiguration configuration)
{
this.authCode = authCode;
this.redirectUri = redirectUri;
this.configuration = configuration;
}
public String getUserId()
{
return (String)claims.get("sub");
}
public Map<String, Object> getClaims()
{
return claims;
}
public Map<String, Object> getResponse()
{
return response;
}
public void redeemAuthCode() throws IOException
{
if (LOG.isDebugEnabled())
LOG.debug("redeemAuthCode() {}", this);
if (authCode != null)
{
try
{
String jwt = getJWT();
decodeJWT(jwt);
if (LOG.isDebugEnabled())
LOG.debug("userInfo {}", claims);
}
finally
{
// reset authCode as it can only be used once
authCode = null;
}
}
}
public boolean validate()
{
if (authCode != null)
return false;
// Check audience should be clientId
String audience = (String)claims.get("aud");
if (!configuration.getIdentityProvider().equals(audience))
{
LOG.warn("Audience claim MUST contain the value of the Issuer Identifier for the OP", this);
//return false;
}
String issuer = (String)claims.get("iss");
if (!configuration.getClientId().equals(issuer))
{
LOG.warn("Issuer claim MUST be the client_id of the OAuth Client {}", this);
//return false;
}
// Check expiry
long expiry = (Long)claims.get("exp");
long currentTimeSeconds = (long)(System.currentTimeMillis() / 1000F);
if (currentTimeSeconds > expiry)
{
if (LOG.isDebugEnabled())
LOG.debug("OpenId Credentials expired {}", this);
return false;
}
return true;
}
private void decodeJWT(String jwt) throws IOException
{
if (LOG.isDebugEnabled())
LOG.debug("decodeJWT {}", jwt);
String[] sections = jwt.split("\\.");
if (sections.length != 3)
throw new IllegalArgumentException("JWT does not contain 3 sections");
String jwtHeaderString = new String(Base64.getDecoder().decode(sections[0]), StandardCharsets.UTF_8);
String jwtClaimString = new String(Base64.getDecoder().decode(sections[1]), StandardCharsets.UTF_8);
String jwtSignature = sections[2];
Map<String, Object> jwtHeader = (Map)JSON.parse(jwtHeaderString);
LOG.debug("JWT Header: {}", jwtHeader);
// validate signature
LOG.warn("Signature NOT validated {}", jwtSignature);
// response should be a set of name/value pairs
claims = (Map)JSON.parse(jwtClaimString);
}
private String getJWT() throws IOException
{
if (LOG.isDebugEnabled())
LOG.debug("getJWT {}", authCode);
// Use the auth code to get the id_token from the OpenID Provider
String urlParameters = "code=" + authCode +
"&client_id=" + configuration.getClientId() +
"&client_secret=" + configuration.getClientSecret() +
"&redirect_uri=" + redirectUri +
"&grant_type=authorization_code";
byte[] payload = urlParameters.getBytes(StandardCharsets.UTF_8);
URL url = new URL(configuration.getTokenEndpoint());
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Host", configuration.getIdentityProvider());
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
connection.setRequestProperty("charset", "utf-8");
try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream()))
{
wr.write(payload);
}
// get response and extract id_token jwt
InputStream content = (InputStream)connection.getContent();
response = (Map)JSON.parse(IO.toString(content));
if (LOG.isDebugEnabled())
LOG.debug("responseMap: {}", response);
return (String)response.get("id_token");
}
}

View File

@ -0,0 +1,134 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.security.openid;
import java.io.IOException;
import java.security.Principal;
import javax.security.auth.Subject;
import javax.servlet.ServletRequest;
import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
public class OpenIdLoginService extends ContainerLifeCycle implements LoginService
{
private static final Logger LOG = Log.getLogger(OpenIdLoginService.class);
private final OpenIdConfiguration _configuration;
private final LoginService loginService;
private IdentityService identityService;
public OpenIdLoginService(OpenIdConfiguration configuration)
{
this(configuration, null);
}
public OpenIdLoginService(OpenIdConfiguration configuration, LoginService loginService)
{
_configuration = configuration;
this.loginService = loginService;
addBean(this.loginService);
}
@Override
public String getName()
{
return _configuration.getIdentityProvider();
}
public OpenIdConfiguration getConfiguration()
{
return _configuration;
}
@Override
public UserIdentity login(String identifier, Object credentials, ServletRequest req)
{
if (LOG.isDebugEnabled())
LOG.debug("login({}, {}, {})", identifier, credentials, req);
OpenIdCredentials openIdCredentials = (OpenIdCredentials)credentials;
try
{
openIdCredentials.redeemAuthCode();
if (!openIdCredentials.validate())
return null;
}
catch (IOException e)
{
LOG.warn(e);
return null;
}
OpenIdUserPrincipal userPrincipal = new OpenIdUserPrincipal(openIdCredentials);
Subject subject = new Subject();
subject.getPrincipals().add(userPrincipal);
subject.getPrivateCredentials().add(credentials);
subject.setReadOnly();
if (loginService != null)
{
UserIdentity userIdentity = loginService.login(openIdCredentials.getUserId(), "", req);
if (userIdentity == null)
return null;
return new OpenIdUserIdentity(subject, userPrincipal, userIdentity);
}
return identityService.newUserIdentity(subject, userPrincipal, new String[0]);
}
@Override
public boolean validate(UserIdentity user)
{
Principal userPrincipal = user.getUserPrincipal();
if (!(userPrincipal instanceof OpenIdUserPrincipal))
return false;
OpenIdCredentials credentials = ((OpenIdUserPrincipal)userPrincipal).getCredentials();
return credentials.validate();
}
@Override
public IdentityService getIdentityService()
{
return loginService == null ? identityService : loginService.getIdentityService();
}
@Override
public void setIdentityService(IdentityService service)
{
if (isRunning())
throw new IllegalStateException("Running");
if (loginService != null)
loginService.setIdentityService(service);
else
identityService = service;
}
@Override
public void logout(UserIdentity user)
{
}
}

View File

@ -0,0 +1,61 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.security.openid;
import java.security.Principal;
import javax.security.auth.Subject;
import org.eclipse.jetty.server.UserIdentity;
public class OpenIdUserIdentity implements UserIdentity
{
private final Subject subject;
private final Principal userPrincipal;
private final UserIdentity userIdentity;
public OpenIdUserIdentity(Subject subject, Principal userPrincipal)
{
this(subject, userPrincipal, null);
}
public OpenIdUserIdentity(Subject subject, Principal userPrincipal, UserIdentity userIdentity)
{
this.subject = subject;
this.userPrincipal = userPrincipal;
this.userIdentity = userIdentity;
}
@Override
public Subject getSubject()
{
return subject;
}
@Override
public Principal getUserPrincipal()
{
return userPrincipal;
}
@Override
public boolean isUserInRole(String role, Scope scope)
{
return userIdentity == null ? false : userIdentity.isUserInRole(role, scope);
}
}

View File

@ -0,0 +1,50 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.security.openid;
import java.io.Serializable;
import java.security.Principal;
public class OpenIdUserPrincipal implements Principal, Serializable
{
private static final long serialVersionUID = -6226920753748399662L;
private final OpenIdCredentials _credentials;
public OpenIdUserPrincipal(OpenIdCredentials credentials)
{
_credentials = credentials;
}
public OpenIdCredentials getCredentials()
{
return _credentials;
}
@Override
public String getName()
{
return _credentials.getUserId();
}
@Override
public String toString()
{
return _credentials.getUserId();
}
}

View File

@ -0,0 +1,217 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.security.openid;
import java.io.IOException;
import java.security.Principal;
import java.util.Map;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.security.Constraint;
public class OpenIdAuthenticationDemo
{
public static class AdminPage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.getWriter().println("<p>this is the admin page "+request.getUserPrincipal()+": <a href=\"/\">Home</a></p>");
}
}
public static class LoginPage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.getWriter().println("<p>you logged in <a href=\"/\">Home</a></p>");
}
}
public static class LogoutPage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
request.getSession().invalidate();
response.sendRedirect("/");
}
}
public static class HomePage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setContentType(MimeTypes.Type.TEXT_HTML.asString());
response.getWriter().println("<h1>Home Page</h1>");
Principal userPrincipal = request.getUserPrincipal();
if (userPrincipal != null)
{
Map<String, Object> userInfo = (Map)request.getSession().getAttribute(OpenIdAuthenticator.__USER_CLAIMS);
response.getWriter().println("<p>Welcome: " + userInfo.get("name") + "</p>");
response.getWriter().println("<a href=\"/profile\">Profile</a><br>");
response.getWriter().println("<a href=\"/admin\">Admin</a><br>");
response.getWriter().println("<a href=\"/logout\">Logout</a><br>");
}
else
{
response.getWriter().println("<p>Please Login <a href=\"/login\">Login</a></p>");
}
}
}
public static class ProfilePage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setContentType(MimeTypes.Type.TEXT_HTML.asString());
Map<String, Object> userInfo = (Map)request.getSession().getAttribute(OpenIdAuthenticator.__USER_CLAIMS);
response.getWriter().println("<!-- Add icon library -->\n" +
"<div class=\"card\">\n" +
" <img src=\""+userInfo.get("picture")+"\" style=\"width:30%\">\n" +
" <h1>"+ userInfo.get("name") +"</h1>\n" +
" <p class=\"title\">"+userInfo.get("email")+"</p>\n" +
" <p>UserId: " + userInfo.get("sub") +"</p>\n" +
"</div>");
response.getWriter().println("<a href=\"/\">Home</a><br>");
response.getWriter().println("<a href=\"/logout\">Logout</a><br>");
}
}
public static class ErrorPage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setContentType(MimeTypes.Type.TEXT_HTML.asString());
response.getWriter().println("<h1>error: not authorized</h1>");
response.getWriter().println("<p>" + request.getUserPrincipal() + "</p>");
}
}
public static void main(String[] args) throws Exception
{
Server server = new Server(8080);
ServletContextHandler context = new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS);
// Add servlets
context.addServlet(ProfilePage.class, "/profile");
context.addServlet(LoginPage.class, "/login");
context.addServlet(AdminPage.class, "/admin");
context.addServlet(LogoutPage.class, "/logout");
context.addServlet(HomePage.class, "/*");
context.addServlet(ErrorPage.class, "/error");
// configure security constraints
Constraint constraint = new Constraint();
constraint.setName(Constraint.__OPENID_AUTH);
constraint.setRoles(new String[]{"**"});
constraint.setAuthenticate(true);
Constraint adminConstraint = new Constraint();
adminConstraint.setName(Constraint.__OPENID_AUTH);
adminConstraint.setRoles(new String[]{"admin"});
adminConstraint.setAuthenticate(true);
// constraint mappings
ConstraintMapping profileMapping = new ConstraintMapping();
profileMapping.setConstraint(constraint);
profileMapping.setPathSpec("/profile");
ConstraintMapping loginMapping = new ConstraintMapping();
loginMapping.setConstraint(constraint);
loginMapping.setPathSpec("/login");
ConstraintMapping adminMapping = new ConstraintMapping();
adminMapping.setConstraint(adminConstraint);
adminMapping.setPathSpec("/admin");
// security handler
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
securityHandler.setRealmName("OpenID Connect Authentication");
securityHandler.addConstraintMapping(profileMapping);
securityHandler.addConstraintMapping(loginMapping);
securityHandler.addConstraintMapping(adminMapping);
// Google Authentication
OpenIdConfiguration configuration = new OpenIdConfiguration(
"https://accounts.google.com/",
"1051168419525-5nl60mkugb77p9j194mrh287p1e0ahfi.apps.googleusercontent.com",
"XT_MIsSv_aUCGollauCaJY8S");
configuration.addScopes("email", "profile");
/*
// Microsoft Authentication
OpenIdConfiguration configuration = new OpenIdConfiguration(
"https://login.microsoftonline.com/common/v2.0",
"5f05dea8-2bd9-45de-b30f-cf5c102b8784",
"IfhQJKi-5[vxhh_=ldqt0y4PkV3z_1ca");
*/
/*
// Yahoo Authentication
OpenIdConfiguration configuration = new OpenIdConfiguration(
"https://login.yahoo.com",
"dj0yJmk9ME5Id05yTkdGNDdPJmQ9WVdrOU9VcHVZWEp4TkdrbWNHbzlNQS0tJnM9Y29uc3VtZXJzZWNyZXQmc3Y9MCZ4PTE2",
"1e7f0eeb0ba0af9d9198f9be760f66ae3ea9e3b5");
configuration.addScopes("sdps-r");
*/
/*
// Create a realm.properties file to associate roles with users
Path tmpDir = Paths.get(System.getProperty("java.io.tmpdir"));
Path tmpPath = Files.createTempFile(tmpDir, "realm", ".properties");
tmpPath.toFile().deleteOnExit();
try (BufferedWriter writer = Files.newBufferedWriter(tmpPath, StandardCharsets.UTF_8, StandardOpenOption.WRITE))
{
// <userId>:[,<rolename> ...]
writer.write("114260987481616800581:,admin");
}
// This must be added to the OpenIdLoginService in constructor below
HashLoginService hashLoginService = new HashLoginService();
hashLoginService.setConfig(tmpPath.toAbsolutePath().toString());
hashLoginService.setHotReload(true);
*/
// Configure OpenIdLoginService optionally providing a base LoginService to provide user roles
OpenIdLoginService loginService = new OpenIdLoginService(configuration);//, hashLoginService);
securityHandler.setLoginService(loginService);
Authenticator authenticator = new OpenIdAuthenticator(configuration, "/error");
securityHandler.setAuthenticator(authenticator);
context.setSecurityHandler(securityHandler);
server.start();
server.join();
}
}

View File

@ -0,0 +1,3 @@
# Setup default logging implementation for during testing
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
# org.eclipse.jetty.security.openid.LEVEL=DEBUG

View File

@ -37,6 +37,8 @@ public class Constraint implements Cloneable, Serializable
public static final String __SPNEGO_AUTH = "SPNEGO";
public static final String __NEGOTIATE_AUTH = "NEGOTIATE";
public static final String __OPENID_AUTH = "OPENID";
public static boolean validateMethod(String method)
{
if (method == null)
@ -48,7 +50,8 @@ public class Constraint implements Cloneable, Serializable
method.equals(__CERT_AUTH) ||
method.equals(__CERT_AUTH2) ||
method.equals(__SPNEGO_AUTH) ||
method.equals(__NEGOTIATE_AUTH));
method.equals(__NEGOTIATE_AUTH) ||
method.equals(__OPENID_AUTH));
}
public static final int DC_UNSET = -1;

View File

@ -94,6 +94,7 @@
<module>jetty-server</module>
<module>jetty-xml</module>
<module>jetty-security</module>
<module>jetty-openid</module>
<module>jetty-servlet</module>
<module>jetty-webapp</module>
<module>jetty-fcgi</module>