diff --git a/jetty-openid/src/main/config/modules/openid.mod b/jetty-openid/src/main/config/modules/openid.mod
index c1070b29910a..4c4cbc18c299 100644
--- a/jetty-openid/src/main/config/modules/openid.mod
+++ b/jetty-openid/src/main/config/modules/openid.mod
@@ -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
diff --git a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java
index ed85e6cc069e..0b6a0a5ad0b4 100644
--- a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java
+++ b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java
@@ -248,6 +248,11 @@ public UserIdentity login(String username, Object credentials, ServletRequest re
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 void logout(ServletRequest request)
}
/**
- * This will attempt to redirect the request to the end_session_endpoint, and finally to the {@link #REDIRECT_PATH}.
+ * This will attempt to redirect the request to the end_session_endpoint, and finally to the {@link #REDIRECT_PATH}.
*
- * 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.
+ * 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.
*
- * 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.
+ * 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.
*
* @param request the request to redirect.
*/
@@ -366,6 +371,17 @@ public void prepareRequest(ServletRequest request)
baseRequest.setMethod(method);
}
+ private boolean hasExpiredIdToken(HttpSession session)
+ {
+ if (session != null)
+ {
+ Map 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 Authentication validateRequest(ServletRequest req, ServletResponse res, b
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 Authentication validateRequest(ServletRequest req, ServletResponse res, b
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 Authentication validateRequest(ServletRequest req, ServletResponse res, b
{
if (LOG.isDebugEnabled())
LOG.debug("auth revoked {}", authentication);
- synchronized (session)
- {
- session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED);
- }
+ logoutWithoutRedirect(request);
}
else
{
@@ -499,10 +525,10 @@ public Authentication validateRequest(ServletRequest req, ServletResponse res, b
}
}
}
+ 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 Authentication validateRequest(ServletRequest req, ServletResponse res, b
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)
diff --git a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java
index 80725f52d350..76c69339d863 100644
--- a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java
+++ b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java
@@ -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 void setAuthenticateNewUsers(boolean authenticateNewUsers)
this.authenticateNewUsers = authenticateNewUsers;
}
+ public boolean isLogoutWhenIdTokenIsExpired()
+ {
+ return logoutWhenIdTokenIsExpired;
+ }
+
+ public void setLogoutWhenIdTokenIsExpired(boolean logoutWhenIdTokenIsExpired)
+ {
+ this.logoutWhenIdTokenIsExpired = logoutWhenIdTokenIsExpired;
+ }
+
private static HttpClient newHttpClient()
{
ClientConnector connector = new ClientConnector();
diff --git a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdCredentials.java b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdCredentials.java
index c705169d709c..df3e18af6a02 100644
--- a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdCredentials.java
+++ b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdCredentials.java
@@ -15,6 +15,7 @@
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 @@ private void validateClaims(OpenIdConfiguration configuration) throws Exception
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 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");
diff --git a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdLoginService.java b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdLoginService.java
index bd67c5bd0069..d33abf907d41 100644
--- a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdLoginService.java
+++ b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdLoginService.java
@@ -136,7 +136,9 @@ public boolean validate(UserIdentity user)
{
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);
}
diff --git a/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdAuthenticationTest.java b/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdAuthenticationTest.java
index 0aa86dfe866c..0c74c9c6e5f2 100644
--- a/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdAuthenticationTest.java
+++ b/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdAuthenticationTest.java
@@ -16,7 +16,10 @@
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 jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
@@ -24,18 +27,24 @@
import jakarta.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.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 configure) throws Exception
{
openIdProvider = new OpenIdProvider(CLIENT_ID, CLIENT_SECRET);
openIdProvider.start();
@@ -100,12 +114,16 @@ public void setup() throws Exception
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 void stop() throws Exception
@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 void testLoginLogout() throws Exception
assertThat(openIdProvider.getLoggedInUsers().getTotal(), equalTo(1L));
}
+ @Test
+ public void testNestedLoginService() throws Exception
+ {
+ AtomicBoolean loggedIn = new AtomicBoolean(true);
+ setup(new AbstractLoginService()
+ {
+
+ @Override
+ protected List 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
diff --git a/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdProvider.java b/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdProvider.java
index 2242eb2c1421..98dc1ec5fbd9 100644
--- a/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdProvider.java
+++ b/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdProvider.java
@@ -16,6 +16,7 @@
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.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 OpenIdProvider(String clientId, String clientSecret)
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 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
}
String scopeString = req.getParameter("scope");
- List scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(StringUtil.csvSplit(scopeString));
+ List 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 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S
}
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 String getSubject()
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 boolean equals(Object obj)
return false;
return Objects.equals(subject, ((User)obj).subject) && Objects.equals(name, ((User)obj).name);
}
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(subject, name);
+ }
}
}
diff --git a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/openid/OpenIdProvider.java b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/openid/OpenIdProvider.java
index 3e4a7ac741ca..7bbd3be29db7 100644
--- a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/openid/OpenIdProvider.java
+++ b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/openid/OpenIdProvider.java
@@ -16,6 +16,7 @@
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.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 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 OpenIdProvider(String clientId, String clientSecret)
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 OpenIdConfiguration getOpenIdConfiguration()
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 void addRedirectUri(String uri)
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 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
}
String scopeString = req.getParameter("scope");
- List scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(StringUtil.csvSplit(scopeString));
+ List 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 void redirectUser(HttpServletRequest request, User user, String redirectU
}
}
- 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 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S
}
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 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
"\"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 String getSubject()
return subject;
}
- public String getIdToken(String provider, String clientId)
+ public String getIdToken(String provider, String clientId, long duration)
+ {
+ 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()
{
- long expiry = System.currentTimeMillis() + Duration.ofMinutes(1).toMillis();
- return JwtEncoder.createIdToken(provider, clientId, subject, name, expiry);
+ return Objects.hash(subject, name);
}
}
}