Issue #9464 - Add optional configuration to log user out after OpenID idToken expires. (Jetty-10) (#9528)

* improvements to logout from the OpenIdLoginService validate
* respect idToken expiry for lifetime of login
* fix checkstyle error
* Add respectIdTokenExpiry configuration
* changes from review
* rename respectIdTokenExpiry to logoutWhenIdTokenIsExpired
* changes from review

---------

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan 2023-04-11 12:20:16 +10:00 committed by GitHub
parent 81efae2f98
commit 24b7d06fd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 365 additions and 47 deletions

View File

@ -38,6 +38,9 @@
<Set name="authenticateNewUsers">
<Property name="jetty.openid.authenticateNewUsers" default="false"/>
</Set>
<Set name="logoutWhenIdTokenIsExpired">
<Property name="jetty.openid.logoutWhenIdTokenIsExpired" default="false"/>
</Set>
<Call name="addScopes">
<Arg>
<Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">

View File

@ -45,3 +45,6 @@ etc/jetty-openid.xml
## What authentication method to use with the Token Endpoint (client_secret_post, client_secret_basic).
# jetty.openid.authMethod=client_secret_post
## Whether the user should be logged out after the idToken expires.
# jetty.openid.logoutWhenIdTokenIsExpired=false

View File

@ -248,6 +248,11 @@ public class OpenIdAuthenticator extends LoginAuthenticator
public void logout(ServletRequest request)
{
attemptLogoutRedirect(request);
logoutWithoutRedirect(request);
}
private void logoutWithoutRedirect(ServletRequest request)
{
super.logout(request);
HttpServletRequest httpRequest = (HttpServletRequest)request;
HttpSession session = httpRequest.getSession(false);
@ -265,13 +270,13 @@ public class OpenIdAuthenticator extends LoginAuthenticator
}
/**
* This will attempt to redirect the request to the end_session_endpoint, and finally to the {@link #REDIRECT_PATH}.
* <p>This will attempt to redirect the request to the end_session_endpoint, and finally to the {@link #REDIRECT_PATH}.</p>
*
* If end_session_endpoint is defined the request will be redirected to the end_session_endpoint, the optional
* post_logout_redirect_uri parameter will be set if {@link #REDIRECT_PATH} is non-null.
* <p>If end_session_endpoint is defined the request will be redirected to the end_session_endpoint, the optional
* post_logout_redirect_uri parameter will be set if {@link #REDIRECT_PATH} is non-null.</p>
*
* If the end_session_endpoint is not defined then the request will be redirected to {@link #REDIRECT_PATH} if it is a
* non-null value, otherwise no redirection will be done.
* <p>If the end_session_endpoint is not defined then the request will be redirected to {@link #REDIRECT_PATH} if it is a
* non-null value, otherwise no redirection will be done.</p>
*
* @param request the request to redirect.
*/
@ -366,6 +371,17 @@ public class OpenIdAuthenticator extends LoginAuthenticator
baseRequest.setMethod(method);
}
private boolean hasExpiredIdToken(HttpSession session)
{
if (session != null)
{
Map<String, Object> claims = (Map)session.getAttribute(CLAIMS);
if (claims != null)
return OpenIdCredentials.checkExpiry(claims);
}
return false;
}
@Override
public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException
{
@ -381,6 +397,17 @@ public class OpenIdAuthenticator extends LoginAuthenticator
if (uri == null)
uri = URIUtil.SLASH;
HttpSession session = request.getSession(false);
if (_openIdConfiguration.isLogoutWhenIdTokenIsExpired() && hasExpiredIdToken(session))
{
// After logout, fall through to the code below and send another login challenge.
logoutWithoutRedirect(request);
// If we expired a valid authentication we do not want to defer authentication,
// we want to try re-authenticate the user.
mandatory = true;
}
mandatory |= isJSecurityCheck(uri);
if (!mandatory)
return new DeferredAuthentication(this);
@ -391,7 +418,9 @@ public class OpenIdAuthenticator extends LoginAuthenticator
try
{
// Get the Session.
HttpSession session = request.getSession();
if (session == null)
session = request.getSession(true);
if (request.isRequestedSessionIdFromURL())
{
sendError(request, response, "Session ID must be a cookie to support OpenID authentication");
@ -464,10 +493,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
{
if (LOG.isDebugEnabled())
LOG.debug("auth revoked {}", authentication);
synchronized (session)
{
session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED);
}
logoutWithoutRedirect(request);
}
else
{
@ -499,10 +525,10 @@ public class OpenIdAuthenticator extends LoginAuthenticator
}
}
}
if (LOG.isDebugEnabled())
LOG.debug("auth {}", authentication);
return authentication;
}
if (LOG.isDebugEnabled())
LOG.debug("auth {}", authentication);
return authentication;
}
// If we can't send challenge.
@ -513,12 +539,11 @@ public class OpenIdAuthenticator extends LoginAuthenticator
return Authentication.UNAUTHENTICATED;
}
// Send the the challenge.
// Send the challenge.
String challengeUri = getChallengeUri(baseRequest);
if (LOG.isDebugEnabled())
LOG.debug("challenge {}->{}", session.getId(), challengeUri);
baseResponse.sendRedirect(challengeUri, true);
return Authentication.SEND_CONTINUE;
}
catch (IOException e)

View File

@ -55,6 +55,7 @@ public class OpenIdConfiguration extends ContainerLifeCycle
private String tokenEndpoint;
private String endSessionEndpoint;
private boolean authenticateNewUsers = false;
private boolean logoutWhenIdTokenIsExpired = false;
/**
* Create an OpenID configuration for a specific OIDC provider.
@ -275,6 +276,16 @@ public class OpenIdConfiguration extends ContainerLifeCycle
this.authenticateNewUsers = authenticateNewUsers;
}
public boolean isLogoutWhenIdTokenIsExpired()
{
return logoutWhenIdTokenIsExpired;
}
public void setLogoutWhenIdTokenIsExpired(boolean logoutWhenIdTokenIsExpired)
{
this.logoutWhenIdTokenIsExpired = logoutWhenIdTokenIsExpired;
}
private static HttpClient newHttpClient()
{
ClientConnector connector = new ClientConnector();

View File

@ -15,6 +15,7 @@ package org.eclipse.jetty.security.openid;
import java.io.Serializable;
import java.net.URI;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@ -137,12 +138,24 @@ public class OpenIdCredentials implements Serializable
throw new AuthenticationException("Authorized party claim value should be the client_id");
// Check that the ID token has not expired by checking the exp claim.
long expiry = (Long)claims.get("exp");
long currentTimeSeconds = (long)(System.currentTimeMillis() / 1000F);
if (currentTimeSeconds > expiry)
if (isExpired())
throw new AuthenticationException("ID Token has expired");
}
public boolean isExpired()
{
return checkExpiry(claims);
}
public static boolean checkExpiry(Map<String, Object> claims)
{
if (claims == null)
return true;
// Check that the ID token has not expired by checking the exp claim.
return Instant.ofEpochSecond((Long)claims.get("exp")).isBefore(Instant.now());
}
private void validateAudience(OpenIdConfiguration configuration) throws AuthenticationException
{
Object aud = claims.get("aud");

View File

@ -136,7 +136,9 @@ public class OpenIdLoginService extends ContainerLifeCycle implements LoginServi
{
if (!(user.getUserPrincipal() instanceof OpenIdUserPrincipal))
return false;
OpenIdUserPrincipal userPrincipal = (OpenIdUserPrincipal)user.getUserPrincipal();
if (configuration.isLogoutWhenIdTokenIsExpired() && userPrincipal.getCredentials().isExpired())
return false;
return loginService == null || loginService.validate(user);
}

View File

@ -16,7 +16,10 @@ package org.eclipse.jetty.security.openid;
import java.io.File;
import java.io.IOException;
import java.security.Principal;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
@ -24,18 +27,24 @@ import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.security.AbstractLoginService;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.RolePrincipal;
import org.eclipse.jetty.security.UserPrincipal;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.server.session.FileSessionDataStoreFactory;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.security.Password;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
@ -43,6 +52,7 @@ import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
@SuppressWarnings("unchecked")
public class OpenIdAuthenticationTest
@ -55,8 +65,12 @@ public class OpenIdAuthenticationTest
private ServerConnector connector;
private HttpClient client;
@BeforeEach
public void setup() throws Exception
public void setup(LoginService loginService) throws Exception
{
setup(loginService, null);
}
public void setup(LoginService loginService, Consumer<OpenIdConfiguration> configure) throws Exception
{
openIdProvider = new OpenIdProvider(CLIENT_ID, CLIENT_SECRET);
openIdProvider.start();
@ -100,12 +114,16 @@ public class OpenIdAuthenticationTest
securityHandler.setAuthMethod(Constraint.__OPENID_AUTH);
securityHandler.setRealmName(openIdProvider.getProvider());
securityHandler.setLoginService(loginService);
securityHandler.addConstraintMapping(profileMapping);
securityHandler.addConstraintMapping(loginMapping);
securityHandler.addConstraintMapping(adminMapping);
// Authentication using local OIDC Provider
server.addBean(new OpenIdConfiguration(openIdProvider.getProvider(), CLIENT_ID, CLIENT_SECRET));
OpenIdConfiguration openIdConfiguration = new OpenIdConfiguration(openIdProvider.getProvider(), CLIENT_ID, CLIENT_SECRET);
if (configure != null)
configure.accept(openIdConfiguration);
server.addBean(openIdConfiguration);
securityHandler.setInitParameter(OpenIdAuthenticator.REDIRECT_PATH, "/redirect_path");
securityHandler.setInitParameter(OpenIdAuthenticator.ERROR_PAGE, "/error");
securityHandler.setInitParameter(OpenIdAuthenticator.LOGOUT_REDIRECT_PATH, "/");
@ -135,6 +153,7 @@ public class OpenIdAuthenticationTest
@Test
public void testLoginLogout() throws Exception
{
setup(null);
openIdProvider.setUser(new OpenIdProvider.User("123456789", "Alice"));
String appUriString = "http://localhost:" + connector.getLocalPort();
@ -188,6 +207,163 @@ public class OpenIdAuthenticationTest
assertThat(openIdProvider.getLoggedInUsers().getTotal(), equalTo(1L));
}
@Test
public void testNestedLoginService() throws Exception
{
AtomicBoolean loggedIn = new AtomicBoolean(true);
setup(new AbstractLoginService()
{
@Override
protected List<RolePrincipal> loadRoleInfo(UserPrincipal user)
{
return List.of(new RolePrincipal("admin"));
}
@Override
protected UserPrincipal loadUserInfo(String username)
{
return new UserPrincipal(username, new Password(""));
}
@Override
public boolean validate(UserIdentity user)
{
if (!loggedIn.get())
return false;
return super.validate(user);
}
});
openIdProvider.setUser(new OpenIdProvider.User("123456789", "Alice"));
String appUriString = "http://localhost:" + connector.getLocalPort();
// Initially not authenticated
ContentResponse response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
String content = response.getContentAsString();
assertThat(content, containsString("not authenticated"));
// Request to login is success
response = client.GET(appUriString + "/login");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString();
assertThat(content, containsString("success"));
// Now authenticated we can get info
response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString();
assertThat(content, containsString("userId: 123456789"));
assertThat(content, containsString("name: Alice"));
assertThat(content, containsString("email: Alice@example.com"));
// The nested login service has supplied the admin role.
response = client.GET(appUriString + "/admin");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
// This causes any validation of UserIdentity in the LoginService to fail
// causing subsequent requests to be redirected to the auth endpoint for login again.
loggedIn.set(false);
client.setFollowRedirects(false);
response = client.GET(appUriString + "/admin");
assertThat(response.getStatus(), is(HttpStatus.SEE_OTHER_303));
String location = response.getHeaders().get(HttpHeader.LOCATION);
assertThat(location, containsString(openIdProvider.getProvider() + "/auth"));
// Note that we couldn't follow "OpenID Connect RP-Initiated Logout 1.0" because we redirect straight to auth endpoint.
assertThat(openIdProvider.getLoggedInUsers().getCurrent(), equalTo(1L));
assertThat(openIdProvider.getLoggedInUsers().getMax(), equalTo(1L));
assertThat(openIdProvider.getLoggedInUsers().getTotal(), equalTo(1L));
}
@Test
public void testExpiredIdToken() throws Exception
{
setup(null, config -> config.setLogoutWhenIdTokenIsExpired(true));
long idTokenExpiryTime = 2000;
openIdProvider.setIdTokenDuration(idTokenExpiryTime);
openIdProvider.setUser(new OpenIdProvider.User("123456789", "Alice"));
String appUriString = "http://localhost:" + connector.getLocalPort();
// Initially not authenticated
ContentResponse response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
String content = response.getContentAsString();
assertThat(content, containsString("not authenticated"));
// Request to login is success
response = client.GET(appUriString + "/login");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString();
assertThat(content, containsString("success"));
// Now authenticated we can get info
client.setFollowRedirects(false);
response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString();
assertThat(content, containsString("userId: 123456789"));
assertThat(content, containsString("name: Alice"));
assertThat(content, containsString("email: Alice@example.com"));
// After waiting past ID_Token expiry time we are no longer authenticated.
// Even though this page is non-mandatory authentication the OpenId attributes should be cleared.
// This then attempts re-authorization the first time even though it is non-mandatory page.
Thread.sleep(idTokenExpiryTime * 2);
response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.SEE_OTHER_303));
assertThat(response.getHeaders().get(HttpHeader.LOCATION), startsWith(openIdProvider.getProvider() + "/auth"));
// User was never redirected to logout page.
assertThat(openIdProvider.getLoggedInUsers().getCurrent(), equalTo(1L));
assertThat(openIdProvider.getLoggedInUsers().getMax(), equalTo(1L));
assertThat(openIdProvider.getLoggedInUsers().getTotal(), equalTo(1L));
}
@Test
public void testExpiredIdTokenDisabled() throws Exception
{
setup(null);
long idTokenExpiryTime = 2000;
openIdProvider.setIdTokenDuration(idTokenExpiryTime);
openIdProvider.setUser(new OpenIdProvider.User("123456789", "Alice"));
String appUriString = "http://localhost:" + connector.getLocalPort();
// Initially not authenticated
ContentResponse response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
String content = response.getContentAsString();
assertThat(content, containsString("not authenticated"));
// Request to login is success
response = client.GET(appUriString + "/login");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString();
assertThat(content, containsString("success"));
// Now authenticated we can get info
client.setFollowRedirects(false);
response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString();
assertThat(content, containsString("userId: 123456789"));
assertThat(content, containsString("name: Alice"));
assertThat(content, containsString("email: Alice@example.com"));
// After waiting past ID_Token expiry time we are still authenticated because logoutWhenIdTokenIsExpired is false by default.
Thread.sleep(idTokenExpiryTime * 2);
response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString();
assertThat(content, containsString("userId: 123456789"));
assertThat(content, containsString("name: Alice"));
assertThat(content, containsString("email: Alice@example.com"));
}
public static class LoginPage extends HttpServlet
{
@Override

View File

@ -16,6 +16,7 @@ package org.eclipse.jetty.security.openid;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -36,7 +37,6 @@ 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;
import org.eclipse.jetty.util.statistic.CounterStatistic;
import org.slf4j.Logger;
@ -61,13 +61,14 @@ public class OpenIdProvider extends ContainerLifeCycle
private String provider;
private User preAuthedUser;
private final CounterStatistic loggedInUsers = new CounterStatistic();
private long _idTokenDuration = Duration.ofSeconds(10).toMillis();
public static void main(String[] args) throws Exception
{
String clientId = "CLIENT_ID123";
String clientSecret = "PASSWORD123";
int port = 5771;
String redirectUri = "http://localhost:8080/openid/auth";
String redirectUri = "http://localhost:8080/j_security_check";
OpenIdProvider openIdProvider = new OpenIdProvider(clientId, clientSecret);
openIdProvider.addRedirectUri(redirectUri);
@ -103,6 +104,16 @@ public class OpenIdProvider extends ContainerLifeCycle
addBean(server);
}
public void setIdTokenDuration(long duration)
{
_idTokenDuration = duration;
}
public long getIdTokenDuration()
{
return _idTokenDuration;
}
public void join() throws InterruptedException
{
server.join();
@ -173,7 +184,7 @@ public class OpenIdProvider extends ContainerLifeCycle
}
String scopeString = req.getParameter("scope");
List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(StringUtil.csvSplit(scopeString));
List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(scopeString.split(" "));
if (!scopes.contains("openid"))
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no openid scope");
@ -286,11 +297,11 @@ public class OpenIdProvider extends ContainerLifeCycle
}
String accessToken = "ABCDEFG";
long expiry = System.currentTimeMillis() + Duration.ofMinutes(10).toMillis();
long accessTokenDuration = Duration.ofMinutes(10).toSeconds();
String response = "{" +
"\"access_token\": \"" + accessToken + "\"," +
"\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId)) + "\"," +
"\"expires_in\": " + expiry + "," +
"\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId, _idTokenDuration)) + "\"," +
"\"expires_in\": " + accessTokenDuration + "," +
"\"token_type\": \"Bearer\"" +
"}";
@ -374,10 +385,10 @@ public class OpenIdProvider extends ContainerLifeCycle
return subject;
}
public String getIdToken(String provider, String clientId)
public String getIdToken(String provider, String clientId, long duration)
{
long expiry = System.currentTimeMillis() + Duration.ofMinutes(1).toMillis();
return JwtEncoder.createIdToken(provider, clientId, subject, name, expiry);
long expiryTime = Instant.now().plusMillis(duration).getEpochSecond();
return JwtEncoder.createIdToken(provider, clientId, subject, name, expiryTime);
}
@Override
@ -387,5 +398,11 @@ public class OpenIdProvider extends ContainerLifeCycle
return false;
return Objects.equals(subject, ((User)obj).subject) && Objects.equals(name, ((User)obj).name);
}
@Override
public int hashCode()
{
return Objects.hash(subject, name);
}
}
}

View File

@ -16,6 +16,7 @@ package org.eclipse.jetty.tests.distribution.openid;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -37,8 +38,8 @@ 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;
import org.eclipse.jetty.util.statistic.CounterStatistic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -49,6 +50,7 @@ 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 static final String END_SESSION_PATH = "/end_session";
private final Map<String, User> issuedAuthCodes = new HashMap<>();
protected final String clientId;
@ -59,13 +61,15 @@ public class OpenIdProvider extends ContainerLifeCycle
private int port = 0;
private String provider;
private User preAuthedUser;
private final CounterStatistic loggedInUsers = new CounterStatistic();
private long _idTokenDuration = Duration.ofSeconds(10).toMillis();
public static void main(String[] args) throws Exception
{
String clientId = "CLIENT_ID123";
String clientSecret = "PASSWORD123";
int port = 5771;
String redirectUri = "http://localhost:8080/openid/auth";
String redirectUri = "http://localhost:8080/j_security_check";
OpenIdProvider openIdProvider = new OpenIdProvider(clientId, clientSecret);
openIdProvider.addRedirectUri(redirectUri);
@ -92,14 +96,25 @@ public class OpenIdProvider extends ContainerLifeCycle
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);
contextHandler.addServlet(new ServletHolder(new ConfigServlet()), CONFIG_PATH);
contextHandler.addServlet(new ServletHolder(new AuthEndpoint()), AUTH_PATH);
contextHandler.addServlet(new ServletHolder(new TokenEndpoint()), TOKEN_PATH);
contextHandler.addServlet(new ServletHolder(new EndSessionEndpoint()), END_SESSION_PATH);
server.setHandler(contextHandler);
addBean(server);
}
public void setIdTokenDuration(long duration)
{
_idTokenDuration = duration;
}
public long getIdTokenDuration()
{
return _idTokenDuration;
}
public void join() throws InterruptedException
{
server.join();
@ -113,6 +128,11 @@ public class OpenIdProvider extends ContainerLifeCycle
return new OpenIdConfiguration(provider, authEndpoint, tokenEndpoint, clientId, clientSecret, null);
}
public CounterStatistic getLoggedInUsers()
{
return loggedInUsers;
}
@Override
protected void doStart() throws Exception
{
@ -145,7 +165,7 @@ public class OpenIdProvider extends ContainerLifeCycle
redirectUris.add(uri);
}
public class OpenIdAuthEndpoint extends HttpServlet
public class AuthEndpoint extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
@ -165,7 +185,7 @@ public class OpenIdProvider extends ContainerLifeCycle
}
String scopeString = req.getParameter("scope");
List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(StringUtil.csvSplit(scopeString));
List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(scopeString.split(" "));
if (!scopes.contains("openid"))
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no openid scope");
@ -253,7 +273,7 @@ public class OpenIdProvider extends ContainerLifeCycle
}
}
public class OpenIdTokenEndpoint extends HttpServlet
private class TokenEndpoint extends HttpServlet
{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
@ -278,20 +298,53 @@ public class OpenIdProvider extends ContainerLifeCycle
}
String accessToken = "ABCDEFG";
long expiry = System.currentTimeMillis() + Duration.ofMinutes(10).toMillis();
long accessTokenDuration = Duration.ofMinutes(10).toSeconds();
String response = "{" +
"\"access_token\": \"" + accessToken + "\"," +
"\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId)) + "\"," +
"\"expires_in\": " + expiry + "," +
"\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId, _idTokenDuration)) + "\"," +
"\"expires_in\": " + accessTokenDuration + "," +
"\"token_type\": \"Bearer\"" +
"}";
loggedInUsers.increment();
resp.setContentType("text/plain");
resp.getWriter().print(response);
}
}
public class OpenIdConfigServlet extends HttpServlet
private class EndSessionEndpoint extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
{
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException
{
String idToken = req.getParameter("id_token_hint");
if (idToken == null)
{
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no id_token_hint");
return;
}
String logoutRedirect = req.getParameter("post_logout_redirect_uri");
if (logoutRedirect == null)
{
resp.setStatus(HttpServletResponse.SC_OK);
resp.getWriter().println("logout success on end_session_endpoint");
return;
}
loggedInUsers.decrement();
resp.setContentType("text/plain");
resp.sendRedirect(logoutRedirect);
}
}
private class ConfigServlet extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
@ -300,6 +353,7 @@ public class OpenIdProvider extends ContainerLifeCycle
"\"issuer\": \"" + provider + "\"," +
"\"authorization_endpoint\": \"" + provider + AUTH_PATH + "\"," +
"\"token_endpoint\": \"" + provider + TOKEN_PATH + "\"," +
"\"end_session_endpoint\": \"" + provider + END_SESSION_PATH + "\"," +
"}";
resp.getWriter().write(discoveryDocument);
@ -332,10 +386,24 @@ public class OpenIdProvider extends ContainerLifeCycle
return subject;
}
public String getIdToken(String provider, String clientId)
public String getIdToken(String provider, String clientId, long duration)
{
long expiry = System.currentTimeMillis() + Duration.ofMinutes(1).toMillis();
return JwtEncoder.createIdToken(provider, clientId, subject, name, expiry);
long expiryTime = Instant.now().plusMillis(duration).getEpochSecond();
return JwtEncoder.createIdToken(provider, clientId, subject, name, expiryTime);
}
@Override
public boolean equals(Object obj)
{
if (!(obj instanceof User))
return false;
return Objects.equals(subject, ((User)obj).subject) && Objects.equals(name, ((User)obj).name);
}
@Override
public int hashCode()
{
return Objects.hash(subject, name);
}
}
}