From 737d27f11ca7d7d384289816c8d232ded8d82398 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Wed, 13 Sep 2023 15:37:43 +0100 Subject: [PATCH] Honor OIDC logout requests when ID token has expired --- .../main/java/io/quarkus/oidc/SecurityEvent.java | 8 ++++++++ .../runtime/CodeAuthenticationMechanism.java | 14 ++++++++++++++ .../src/main/resources/application.properties | 2 ++ .../io/quarkus/it/keycloak/CodeFlowTest.java | 16 ++++++++++++++++ .../keycloak/KeycloakRealmResourceManager.java | 2 +- 5 files changed, 41 insertions(+), 1 deletion(-) diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/SecurityEvent.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/SecurityEvent.java index 078d2d6c8f036..a03503bdb3c91 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/SecurityEvent.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/SecurityEvent.java @@ -9,6 +9,8 @@ * */ public class SecurityEvent { + public static final String SESSION_TOKENS_PROPERTY = "session-tokens"; + public enum Type { /** * OIDC Login event which is reported after the first user authentication but also when the user's session @@ -30,6 +32,12 @@ public enum Type { */ OIDC_LOGOUT_RP_INITIATED, + /** + * OIDC Logout event is reported when the current user has started an RP-initiated OIDC logout flow but the session has + * already expired. + */ + OIDC_LOGOUT_RP_INITIATED_SESSION_EXPIRED, + /** * OIDC BackChannel Logout initiated event is reported when the BackChannel logout request to logout the current user * has been received. diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java index 6ed98a9c7b61d..be150ea8a97a0 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java @@ -354,6 +354,14 @@ public Uni apply(Throwable t) { t.getCause()))); } // Token has expired, try to refresh + if (isRpInitiatedLogout(context, configContext)) { + LOG.debug("Session has expired, performing an RP initiated logout"); + fireEvent(SecurityEvent.Type.OIDC_LOGOUT_RP_INITIATED_SESSION_EXPIRED, + Map.of(SecurityEvent.SESSION_TOKENS_PROPERTY, session)); + return Uni.createFrom().item((SecurityIdentity) null) + .call(() -> buildLogoutRedirectUriUni(context, configContext, + currentIdToken)); + } if (session.getRefreshToken() == null) { LOG.debug( "Token has expired, token refresh is not possible because the refresh token is null"); @@ -983,6 +991,12 @@ private void fireEvent(SecurityEvent.Type eventType, SecurityIdentity securityId } } + private void fireEvent(SecurityEvent.Type eventType, Map properties) { + if (resolver.isSecurityEventObserved()) { + resolver.getSecurityEvent().fire(new SecurityEvent(eventType, properties)); + } + } + private String getRedirectPath(OidcTenantConfig oidcConfig, RoutingContext context) { Authentication auth = oidcConfig.getAuthentication(); return auth.getRedirectPath().isPresent() ? auth.getRedirectPath().get() : context.request().path(); diff --git a/integration-tests/oidc-code-flow/src/main/resources/application.properties b/integration-tests/oidc-code-flow/src/main/resources/application.properties index 3f92f20e648f5..5799a66d05af4 100644 --- a/integration-tests/oidc-code-flow/src/main/resources/application.properties +++ b/integration-tests/oidc-code-flow/src/main/resources/application.properties @@ -80,6 +80,8 @@ quarkus.oidc.tenant-logout.application-type=web-app quarkus.oidc.tenant-logout.authentication.cookie-path=/tenant-logout quarkus.oidc.tenant-logout.logout.path=/tenant-logout/logout quarkus.oidc.tenant-logout.logout.post-logout-path=/tenant-logout/post-logout +quarkus.oidc.tenant-logout.authentication.session-age-extension=2M +quarkus.oidc.tenant-logout.token.refresh-expired=true quarkus.oidc.tenant-refresh.auth-server-url=${keycloak.url}/realms/logout-realm quarkus.oidc.tenant-refresh.client-id=quarkus-app diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java index ae138fd9d60b3..8d55c8193dc06 100644 --- a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java +++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java @@ -611,6 +611,22 @@ public void testRPInitiatedLogout() throws IOException { page = webClient.getPage("http://localhost:8081/tenant-logout"); assertEquals("Sign in to logout-realm", page.getTitleText()); + + // login again + loginForm = page.getForms().get(0); + loginForm.getInputByName("username").setValueAttribute("alice"); + loginForm.getInputByName("password").setValueAttribute("alice"); + page = loginForm.getInputByName("login").click(); + assertEquals("Tenant Logout, refreshed: false", page.asNormalizedText()); + + assertNotNull(getSessionCookie(webClient, "tenant-logout")); + + await().atLeast(Duration.ofSeconds(11)); + + page = webClient.getPage("http://localhost:8081/tenant-logout/logout"); + assertTrue(page.asNormalizedText().contains("You were logged out, please login again")); + assertNull(getSessionCookie(webClient, "tenant-logout")); + webClient.getCookieManager().clearCookies(); } } diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java index b9c0be214ee86..fd749a8f8668d 100644 --- a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java +++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java @@ -35,7 +35,7 @@ public Map start() { // revoke refresh tokens so that they can only be used once logoutRealm.setRevokeRefreshToken(true); logoutRealm.setRefreshTokenMaxReuse(0); - logoutRealm.setSsoSessionMaxLifespan(15); + logoutRealm.setSsoSessionMaxLifespan(10); logoutRealm.setAccessTokenLifespan(5); client.createRealm(logoutRealm); realms.add(logoutRealm);