Merge pull request #4038 from eclipse/jetty-9.4.x-OpenId
OpenID Connect Authentication
This commit is contained in:
commit
e013c24326
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.toolchain</groupId>
|
||||
<artifactId>jetty-test-helper</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-client</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0"?>
|
||||
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.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.openIdProvider"/></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>
|
||||
<Call name="authenticateNewUsers">
|
||||
<Arg type="boolean">
|
||||
<Property name="jetty.openid.authenticateNewUsers" default="false"/>
|
||||
</Arg>
|
||||
</Call>
|
||||
</New>
|
||||
</Arg>
|
||||
</Call>
|
||||
</Configure>
|
|
@ -0,0 +1,34 @@
|
|||
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]
|
||||
## The OpenID Identity Provider
|
||||
# jetty.openid.openIdProvider=https://accounts.google.com/
|
||||
|
||||
## The Client Identifier
|
||||
# jetty.openid.clientId=test1234.apps.googleusercontent.com
|
||||
|
||||
## The Client Secret
|
||||
# jetty.openid.clientSecret=XT_Mafv_aUCGheuCaKY8P
|
||||
|
||||
## Additional Scopes to Request
|
||||
# jetty.openid.scopes=email,profile
|
||||
|
||||
## Whether to Authenticate users not found by base LoginService
|
||||
# jetty.openid.authenticateNewUsers=false
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0"?>
|
||||
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.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"><SystemProperty name="jetty.home" default="."/>/etc/realm.properties</Set>
|
||||
<Set name="hotReload">true</Set>
|
||||
</New>
|
||||
-->
|
||||
</Configure>
|
|
@ -0,0 +1,491 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.nio.charset.StandardCharsets;
|
||||
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.UrlEncoded;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
import org.eclipse.jetty.util.security.Constraint;
|
||||
|
||||
/**
|
||||
* <p>Implements authentication using OpenId Connect on top of OAuth 2.0.
|
||||
*
|
||||
* <p>The OpenIdAuthenticator redirects unauthenticated requests to the OpenID Connect Provider. The End-User is
|
||||
* eventually redirected back with an Authorization Code to the /j_security_check URI within the context.
|
||||
* The Authorization Code is then used to authenticate the user through the {@link OpenIdCredentials} and {@link OpenIdLoginService}.
|
||||
* </p>
|
||||
* <p>
|
||||
* Once a user is authenticated the OpenID Claims can be retrieved through an attribute on the session with the key {@link #CLAIMS}.
|
||||
* The full response containing the OAuth 2.0 Access Token can be obtained with the session attribute {@link #RESPONSE}.
|
||||
* </p>
|
||||
* <p>{@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 CLAIMS = "org.eclipse.jetty.security.openid.claims";
|
||||
public static final String RESPONSE = "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.
|
||||
*
|
||||
* @param alwaysSave true to always save the uri
|
||||
*/
|
||||
public void setAlwaysSaveUri(boolean alwaysSave)
|
||||
{
|
||||
_alwaysSaveUri = alwaysSave;
|
||||
}
|
||||
|
||||
public boolean isAlwaysSaveUri()
|
||||
{
|
||||
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(CLAIMS, ((OpenIdCredentials)credentials).getClaims());
|
||||
session.setAttribute(RESPONSE, ((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(CLAIMS);
|
||||
session.removeAttribute(RESPONSE);
|
||||
}
|
||||
|
||||
@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);
|
||||
if (LOG.isDebugEnabled())
|
||||
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()))
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
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
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("auth retry {}->{}", authentication, jUri);
|
||||
StringBuffer buf = request.getRequestURL();
|
||||
if (request.getQueryString() != null)
|
||||
buf.append("?").append(request.getQueryString());
|
||||
|
||||
if (jUri.equals(buf.toString()))
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
MultiMap<String> jPost = (MultiMap<String>)session.getAttribute(J_POST);
|
||||
if (jPost != null)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("auth rePOST {}->{}", authentication, jUri);
|
||||
baseRequest.setContentParameters(jPost);
|
||||
}
|
||||
session.removeAttribute(J_URI);
|
||||
session.removeAttribute(J_METHOD);
|
||||
session.removeAttribute(J_POST);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("auth {}", authentication);
|
||||
return authentication;
|
||||
}
|
||||
}
|
||||
|
||||
// if we can't send challenge
|
||||
if (DeferredAuthentication.isDeferred(response))
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
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 || isAlwaysSaveUri())
|
||||
{
|
||||
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);
|
||||
if (LOG.isDebugEnabled())
|
||||
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(" ").append(s);
|
||||
}
|
||||
|
||||
return _configuration.getAuthEndpoint() +
|
||||
"?client_id=" + UrlEncoded.encodeString(_configuration.getClientId(), StandardCharsets.UTF_8) +
|
||||
"&redirect_uri=" + UrlEncoded.encodeString(getRedirectUri(request), StandardCharsets.UTF_8) +
|
||||
"&scope=openid" + UrlEncoded.encodeString(scopes.toString(), StandardCharsets.UTF_8) +
|
||||
"&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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.io.Serializable;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Holds the configuration for an OpenID Connect service.
|
||||
*
|
||||
* This uses the OpenID Provider URL with the path {@link #CONFIG_PATH} to discover
|
||||
* the required information about the OIDC service.
|
||||
*/
|
||||
public class OpenIdConfiguration implements Serializable
|
||||
{
|
||||
private static final Logger LOG = Log.getLogger(OpenIdConfiguration.class);
|
||||
private static final long serialVersionUID = 2227941990601349102L;
|
||||
private static final String CONFIG_PATH = "/.well-known/openid-configuration";
|
||||
|
||||
private final String openIdProvider;
|
||||
private final String issuer;
|
||||
private final String authEndpoint;
|
||||
private final String tokenEndpoint;
|
||||
private final String clientId;
|
||||
private final String clientSecret;
|
||||
private final Map<String, Object> discoveryDocument;
|
||||
private final List<String> scopes = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Create an OpenID configuration for a specific OIDC provider.
|
||||
* @param provider The URL of the OpenID provider.
|
||||
* @param clientId OAuth 2.0 Client Identifier valid at the Authorization Server.
|
||||
* @param clientSecret The client secret known only by the Client and the Authorization Server.
|
||||
*/
|
||||
public OpenIdConfiguration(String provider, String clientId, String clientSecret)
|
||||
{
|
||||
this.openIdProvider = 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);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("discovery document {}", discoveryDocument);
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
throw new IllegalArgumentException("invalid identity provider", e);
|
||||
}
|
||||
|
||||
issuer = (String)discoveryDocument.get("issuer");
|
||||
if (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 getIssuer()
|
||||
{
|
||||
return issuer;
|
||||
}
|
||||
|
||||
public String getOpenIdProvider()
|
||||
{
|
||||
return openIdProvider;
|
||||
}
|
||||
|
||||
public String getTokenEndpoint()
|
||||
{
|
||||
return tokenEndpoint;
|
||||
}
|
||||
|
||||
public void addScopes(String... scopes)
|
||||
{
|
||||
Collections.addAll(this.scopes, scopes);
|
||||
}
|
||||
|
||||
public List<String> getScopes()
|
||||
{
|
||||
return scopes;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.io.Serializable;
|
||||
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.UrlEncoded;
|
||||
import org.eclipse.jetty.util.ajax.JSON;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
/**
|
||||
* <p>The credentials of an user to be authenticated with OpenID Connect. This will contain
|
||||
* the OpenID ID Token and the OAuth 2.0 Access Token.</p>
|
||||
*
|
||||
* <p>
|
||||
* This is constructed with an authorization code from the authentication request. This authorization code
|
||||
* is then exchanged using {@link #redeemAuthCode()} for a response containing the ID Token and Access Token.
|
||||
* The response is then validated against the {@link OpenIdConfiguration}.
|
||||
* </p>
|
||||
*/
|
||||
public class OpenIdCredentials implements Serializable
|
||||
{
|
||||
private static final Logger LOG = Log.getLogger(OpenIdCredentials.class);
|
||||
private static final long serialVersionUID = 4766053233370044796L;
|
||||
|
||||
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
|
||||
{
|
||||
response = claimAuthCode(authCode);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("response: {}", response);
|
||||
|
||||
String idToken = (String)response.get("id_token");
|
||||
if (idToken == null)
|
||||
throw new IllegalArgumentException("no id_token");
|
||||
|
||||
String accessToken = (String)response.get("access_token");
|
||||
if (accessToken == null)
|
||||
throw new IllegalArgumentException("no access_token");
|
||||
|
||||
String tokenType = (String)response.get("token_type");
|
||||
if (!"Bearer".equalsIgnoreCase(tokenType))
|
||||
throw new IllegalArgumentException("invalid token_type");
|
||||
|
||||
claims = decodeJWT(idToken);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("claims {}", claims);
|
||||
validateClaims();
|
||||
}
|
||||
finally
|
||||
{
|
||||
// reset authCode as it can only be used once
|
||||
authCode = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateClaims()
|
||||
{
|
||||
// Issuer Identifier for the OpenID Provider MUST exactly match the value of the iss (issuer) Claim.
|
||||
if (!configuration.getIssuer().equals(claims.get("iss")))
|
||||
throw new IllegalArgumentException("Issuer Identifier MUST exactly match the iss Claim");
|
||||
|
||||
// The aud (audience) Claim MUST contain the client_id value.
|
||||
if (!configuration.getClientId().equals(claims.get("aud")))
|
||||
throw new IllegalArgumentException("Audience Claim MUST contain the client_id value");
|
||||
|
||||
// If an azp (authorized party) Claim is present, verify that its client_id is the Claim Value.
|
||||
Object azp = claims.get("azp");
|
||||
if (azp != null && !configuration.getClientId().equals(azp))
|
||||
throw new IllegalArgumentException("Authorized party claim value should be the client_id");
|
||||
}
|
||||
|
||||
public boolean isExpired()
|
||||
{
|
||||
if (authCode != null || claims == null)
|
||||
return true;
|
||||
|
||||
// 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 true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected Map<String, Object> 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");
|
||||
|
||||
Base64.Decoder decoder = Base64.getDecoder();
|
||||
String jwtHeaderString = new String(decoder.decode(sections[0]), StandardCharsets.UTF_8);
|
||||
String jwtClaimString = new String(decoder.decode(sections[1]), StandardCharsets.UTF_8);
|
||||
String jwtSignature = sections[2];
|
||||
|
||||
Map<String, Object> jwtHeader = (Map)JSON.parse(jwtHeaderString);
|
||||
LOG.debug("JWT Header: {}", jwtHeader);
|
||||
|
||||
/* If the ID Token is received via direct communication between the Client
|
||||
and the Token Endpoint (which it is in this flow), the TLS server validation
|
||||
MAY be used to validate the issuer in place of checking the token signature. */
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("JWT signature not validated {}", jwtSignature);
|
||||
|
||||
return (Map)JSON.parse(jwtClaimString);
|
||||
}
|
||||
|
||||
private Map<String, Object> claimAuthCode(String authCode) throws IOException
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("claimAuthCode {}", authCode);
|
||||
|
||||
// Use the authorization code to get the id_token from the OpenID Provider
|
||||
String urlParameters = "code=" + authCode +
|
||||
"&client_id=" + UrlEncoded.encodeString(configuration.getClientId(), StandardCharsets.UTF_8) +
|
||||
"&client_secret=" + UrlEncoded.encodeString(configuration.getClientSecret(), StandardCharsets.UTF_8) +
|
||||
"&redirect_uri=" + UrlEncoded.encodeString(redirectUri, StandardCharsets.UTF_8) +
|
||||
"&grant_type=authorization_code";
|
||||
|
||||
URL url = new URL(configuration.getTokenEndpoint());
|
||||
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
|
||||
try
|
||||
{
|
||||
connection.setDoOutput(true);
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setRequestProperty("Host", configuration.getOpenIdProvider());
|
||||
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
|
||||
|
||||
try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream()))
|
||||
{
|
||||
wr.write(urlParameters.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
try (InputStream content = (InputStream)connection.getContent())
|
||||
{
|
||||
return (Map)JSON.parse(IO.toString(content));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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 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;
|
||||
|
||||
/**
|
||||
* The implementation of {@link LoginService} required to use OpenID Connect.
|
||||
*
|
||||
* <p>
|
||||
* Can contain an optional wrapped {@link LoginService} which is used to store role information about users.
|
||||
* </p>
|
||||
*/
|
||||
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;
|
||||
private boolean authenticateNewUsers;
|
||||
|
||||
public OpenIdLoginService(OpenIdConfiguration configuration)
|
||||
{
|
||||
this(configuration, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a wrapped {@link LoginService} to store information about user roles.
|
||||
* Users in the wrapped loginService must be stored with their username as
|
||||
* the value of the sub (subject) Claim, and a credentials value of the empty string.
|
||||
* @param configuration the OpenID configuration to use.
|
||||
* @param loginService the wrapped LoginService to defer to for user roles.
|
||||
*/
|
||||
public OpenIdLoginService(OpenIdConfiguration configuration, LoginService loginService)
|
||||
{
|
||||
_configuration = configuration;
|
||||
this.loginService = loginService;
|
||||
addBean(this.loginService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName()
|
||||
{
|
||||
return _configuration.getOpenIdProvider();
|
||||
}
|
||||
|
||||
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.isExpired())
|
||||
return null;
|
||||
}
|
||||
catch (Throwable 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)
|
||||
{
|
||||
if (isAuthenticateNewUsers())
|
||||
return getIdentityService().newUserIdentity(subject, userPrincipal, new String[0]);
|
||||
return null;
|
||||
}
|
||||
return new OpenIdUserIdentity(subject, userPrincipal, userIdentity);
|
||||
}
|
||||
|
||||
return identityService.newUserIdentity(subject, userPrincipal, new String[0]);
|
||||
}
|
||||
|
||||
public boolean isAuthenticateNewUsers()
|
||||
{
|
||||
return authenticateNewUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* This setting is only meaningful if a wrapped {@link LoginService} has been set.
|
||||
* <p>
|
||||
* If set to true, any users not found by the wrapped {@link LoginService} will still
|
||||
* be authenticated but with no roles, if set to false users will not be
|
||||
* authenticated unless they are discovered by the wrapped {@link LoginService}.
|
||||
* </p>
|
||||
* @param authenticateNewUsers whether to authenticate users not found by a wrapping LoginService
|
||||
*/
|
||||
public void setAuthenticateNewUsers(boolean authenticateNewUsers)
|
||||
{
|
||||
this.authenticateNewUsers = authenticateNewUsers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validate(UserIdentity user)
|
||||
{
|
||||
Principal userPrincipal = user.getUserPrincipal();
|
||||
if (!(userPrincipal instanceof OpenIdUserPrincipal))
|
||||
return false;
|
||||
|
||||
OpenIdCredentials credentials = ((OpenIdUserPrincipal)userPrincipal).getCredentials();
|
||||
return !credentials.isExpired();
|
||||
}
|
||||
|
||||
@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)
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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, 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 && userIdentity.isUserInRole(role, scope);
|
||||
}
|
||||
}
|
|
@ -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 = 1521094652756670469L;
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,226 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.security.Authenticator;
|
||||
import org.eclipse.jetty.security.ConstraintMapping;
|
||||
import org.eclipse.jetty.security.ConstraintSecurityHandler;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.util.security.Constraint;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
public class OpenIdAuthenticationTest
|
||||
{
|
||||
public static final String CLIENT_ID = "testClient101";
|
||||
public static final String CLIENT_SECRET = "secret37989798";
|
||||
|
||||
private OpenIdProvider openIdProvider;
|
||||
private Server server;
|
||||
private ServerConnector connector;
|
||||
private HttpClient client;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception
|
||||
{
|
||||
openIdProvider = new OpenIdProvider(CLIENT_ID, CLIENT_SECRET);
|
||||
openIdProvider.start();
|
||||
|
||||
server = new Server();
|
||||
connector = new ServerConnector(server);
|
||||
server.addConnector(connector);
|
||||
ServletContextHandler context = new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS);
|
||||
|
||||
// Add servlets
|
||||
context.addServlet(LoginPage.class, "/login");
|
||||
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);
|
||||
|
||||
// Authentication using local OIDC Provider
|
||||
OpenIdConfiguration configuration = new OpenIdConfiguration(openIdProvider.getProvider(), CLIENT_ID, CLIENT_SECRET);
|
||||
|
||||
// Configure OpenIdLoginService optionally providing a base LoginService to provide user roles
|
||||
OpenIdLoginService loginService = new OpenIdLoginService(configuration);//, hashLoginService);
|
||||
securityHandler.setLoginService(loginService);
|
||||
|
||||
Authenticator authenticator = new OpenIdAuthenticator(configuration, "/error");
|
||||
securityHandler.setAuthenticator(authenticator);
|
||||
context.setSecurityHandler(securityHandler);
|
||||
|
||||
server.start();
|
||||
String redirectUri = "http://localhost:"+connector.getLocalPort() + "/j_security_check";
|
||||
openIdProvider.addRedirectUri(redirectUri);
|
||||
|
||||
client = new HttpClient();
|
||||
client.start();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void stop() throws Exception
|
||||
{
|
||||
openIdProvider.stop();
|
||||
server.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoginLogout() throws Exception
|
||||
{
|
||||
String appUriString = "http://localhost:"+connector.getLocalPort();
|
||||
|
||||
// Initially not authenticated
|
||||
ContentResponse response = client.GET(appUriString + "/");
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
String[] content = response.getContentAsString().split("\n");
|
||||
assertThat(content.length, is(1));
|
||||
assertThat(content[0], is("not authenticated"));
|
||||
|
||||
// Request to login is success
|
||||
response = client.GET(appUriString + "/login");
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
content = response.getContentAsString().split("\n");
|
||||
assertThat(content.length, is(1));
|
||||
assertThat(content[0], is("success"));
|
||||
|
||||
// Now authenticated we can get info
|
||||
response = client.GET(appUriString + "/");
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
content = response.getContentAsString().split("\n");
|
||||
assertThat(content.length, is(3));
|
||||
assertThat(content[0], is("userId: 123456789"));
|
||||
assertThat(content[1], is("name: FirstName LastName"));
|
||||
assertThat(content[2], is("email: FirstName@fake-email.com"));
|
||||
|
||||
// Request to admin page gives 403 as we do not have admin role
|
||||
response = client.GET(appUriString + "/admin");
|
||||
assertThat(response.getStatus(), is(HttpStatus.FORBIDDEN_403));
|
||||
|
||||
// We are no longer authenticated after logging out
|
||||
response = client.GET(appUriString + "/logout");
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
content = response.getContentAsString().split("\n");
|
||||
assertThat(content.length, is(1));
|
||||
assertThat(content[0], is("not authenticated"));
|
||||
}
|
||||
|
||||
public static class LoginPage extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
|
||||
{
|
||||
response.getWriter().println("success");
|
||||
}
|
||||
}
|
||||
|
||||
public static class LogoutPage extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
|
||||
{
|
||||
request.getSession().invalidate();
|
||||
response.sendRedirect("/");
|
||||
}
|
||||
}
|
||||
|
||||
public static class AdminPage extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
|
||||
{
|
||||
Map<String, Object> userInfo = (Map)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
|
||||
response.getWriter().println(userInfo.get("sub") + ": success");
|
||||
}
|
||||
}
|
||||
|
||||
public static class HomePage extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
|
||||
{
|
||||
response.setContentType("text/plain");
|
||||
Principal userPrincipal = request.getUserPrincipal();
|
||||
if (userPrincipal != null)
|
||||
{
|
||||
Map<String, Object> userInfo = (Map)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
|
||||
response.getWriter().println("userId: " + userInfo.get("sub"));
|
||||
response.getWriter().println("name: " + userInfo.get("name"));
|
||||
response.getWriter().println("email: " + userInfo.get("email"));
|
||||
}
|
||||
else
|
||||
{
|
||||
response.getWriter().println("not authenticated");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ErrorPage extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
|
||||
{
|
||||
response.setContentType("text/plain");
|
||||
response.getWriter().println("not authorized");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,254 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.component.ContainerLifeCycle;
|
||||
|
||||
public class OpenIdProvider extends ContainerLifeCycle
|
||||
{
|
||||
private static final String CONFIG_PATH = "/.well-known/openid-configuration";
|
||||
private static final String AUTH_PATH = "/auth";
|
||||
private static final String TOKEN_PATH = "/token";
|
||||
private final Map<String, User> issuedAuthCodes = new HashMap<>();
|
||||
|
||||
protected final String clientId;
|
||||
protected final String clientSecret;
|
||||
protected final List<String> redirectUris = new ArrayList<>();
|
||||
|
||||
private String provider;
|
||||
private Server server;
|
||||
private ServerConnector connector;
|
||||
|
||||
public OpenIdProvider(String clientId, String clientSecret)
|
||||
{
|
||||
this.clientId = clientId;
|
||||
this.clientSecret = clientSecret;
|
||||
|
||||
server = new Server();
|
||||
connector = new ServerConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
ServletContextHandler contextHandler = new ServletContextHandler();
|
||||
contextHandler.setContextPath("/");
|
||||
contextHandler.addServlet(new ServletHolder(new OpenIdConfigServlet()), CONFIG_PATH);
|
||||
contextHandler.addServlet(new ServletHolder(new OpenIdAuthEndpoint()), AUTH_PATH);
|
||||
contextHandler.addServlet(new ServletHolder(new OpenIdTokenEndpoint()), TOKEN_PATH);
|
||||
server.setHandler(contextHandler);
|
||||
|
||||
addBean(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStart() throws Exception
|
||||
{
|
||||
super.doStart();
|
||||
provider = "http://localhost:" + connector.getLocalPort();
|
||||
}
|
||||
|
||||
public String getProvider()
|
||||
{
|
||||
if (!isStarted())
|
||||
throw new IllegalStateException();
|
||||
return provider;
|
||||
}
|
||||
|
||||
public void addRedirectUri(String uri)
|
||||
{
|
||||
redirectUris.add(uri);
|
||||
}
|
||||
|
||||
public class OpenIdAuthEndpoint extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
if (!clientId.equals(req.getParameter("client_id")))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid client_id");
|
||||
return;
|
||||
}
|
||||
|
||||
String redirectUri = req.getParameter("redirect_uri");
|
||||
if (!redirectUris.contains(redirectUri))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri");
|
||||
return;
|
||||
}
|
||||
|
||||
String scopeString = req.getParameter("scope");
|
||||
List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(StringUtil.csvSplit(scopeString));
|
||||
if (!scopes.contains("openid"))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no openid scope");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!"code".equals(req.getParameter("response_type")))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "response_type must be code");
|
||||
return;
|
||||
}
|
||||
|
||||
String state = req.getParameter("state");
|
||||
if (state == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param");
|
||||
return;
|
||||
}
|
||||
|
||||
String authCode = UUID.randomUUID().toString().replace("-", "");
|
||||
User user = new User(123456789, "FirstName", "LastName");
|
||||
issuedAuthCodes.put(authCode, user);
|
||||
|
||||
final Request baseRequest = Request.getBaseRequest(req);
|
||||
final Response baseResponse = baseRequest.getResponse();
|
||||
redirectUri += "?code=" + authCode + "&state=" + state;
|
||||
int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ?
|
||||
HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
|
||||
baseResponse.sendRedirect(redirectCode, resp.encodeRedirectURL(redirectUri));
|
||||
}
|
||||
}
|
||||
|
||||
public class OpenIdTokenEndpoint extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
String code = req.getParameter("code");
|
||||
|
||||
if (!clientId.equals(req.getParameter("client_id")) ||
|
||||
!clientSecret.equals(req.getParameter("client_secret")) ||
|
||||
!redirectUris.contains(req.getParameter("redirect_uri")) ||
|
||||
!"authorization_code".equals(req.getParameter("grant_type")) ||
|
||||
code == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "bad auth request");
|
||||
return;
|
||||
}
|
||||
|
||||
User user = issuedAuthCodes.remove(code);
|
||||
if (user == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid auth code");
|
||||
return;
|
||||
}
|
||||
|
||||
String jwtHeader = "{\"INFO\": \"this is not used or checked in our implementation\"}";
|
||||
String jwtBody = user.getIdToken();
|
||||
String jwtSignature = "we do not validate signature as we use the authorization code flow";
|
||||
|
||||
Base64.Encoder encoder = Base64.getEncoder();
|
||||
String jwt = encoder.encodeToString(jwtHeader.getBytes()) + "." +
|
||||
encoder.encodeToString(jwtBody.getBytes()) + "." +
|
||||
encoder.encodeToString(jwtSignature.getBytes());
|
||||
|
||||
String accessToken = "ABCDEFG";
|
||||
long expiry = System.currentTimeMillis() + Duration.ofMinutes(10).toMillis();
|
||||
String response = "{" +
|
||||
"\"access_token\": \"" + accessToken + "\"," +
|
||||
"\"id_token\": \"" + jwt + "\"," +
|
||||
"\"expires_in\": " + expiry + "," +
|
||||
"\"token_type\": \"Bearer\"" +
|
||||
"}";
|
||||
|
||||
resp.setContentType("text/plain");
|
||||
resp.getWriter().print(response);
|
||||
}
|
||||
}
|
||||
|
||||
public class OpenIdConfigServlet extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
String discoveryDocument = "{" +
|
||||
"\"issuer\": \"" + provider + "\"," +
|
||||
"\"authorization_endpoint\": \"" + provider + AUTH_PATH + "\"," +
|
||||
"\"token_endpoint\": \"" + provider + TOKEN_PATH + "\"," +
|
||||
"}";
|
||||
|
||||
resp.getWriter().write(discoveryDocument);
|
||||
}
|
||||
}
|
||||
|
||||
public class User
|
||||
{
|
||||
private long subject;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
|
||||
public User(String firstName, String lastName)
|
||||
{
|
||||
this(new Random().nextLong(), firstName, lastName);
|
||||
}
|
||||
|
||||
public User(long subject, String firstName, String lastName)
|
||||
{
|
||||
this.subject = subject;
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getFirstName()
|
||||
{
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public String getLastName()
|
||||
{
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public String getIdToken()
|
||||
{
|
||||
return "{" +
|
||||
"\"iss\": \"" + provider + "\"," +
|
||||
"\"sub\": \"" + subject + "\"," +
|
||||
"\"aud\": \"" + clientId + "\"," +
|
||||
"\"exp\": " + System.currentTimeMillis() + Duration.ofMinutes(1).toMillis() + "," +
|
||||
"\"name\": \"" + firstName + " " + lastName + "\"," +
|
||||
"\"email\": \"" + firstName + "@fake-email.com" + "\"" +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
|
||||
# org.eclipse.jetty.LEVEL=DEBUG
|
||||
# org.eclipse.jetty.security.openid.LEVEL=DEBUG
|
|
@ -36,6 +36,7 @@ public class Constraint implements Cloneable, Serializable
|
|||
public static final String __CERT_AUTH2 = "CLIENT-CERT";
|
||||
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)
|
||||
{
|
||||
|
@ -48,7 +49,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;
|
||||
|
|
Loading…
Reference in New Issue