diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutTokenCache.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutTokenCache.java index 096f5b4b0923e..4150096851cf2 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutTokenCache.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutTokenCache.java @@ -45,6 +45,10 @@ public TokenVerificationResult removeTokenVerification(String token) { return entry == null ? null : entry.result; } + public boolean containsTokenVerification(String token) { + return cacheMap.containsKey(token); + } + public void clearCache() { cacheMap.clear(); size.set(0); 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 a9a9699eecdab..435cefdf313d0 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 @@ -275,30 +275,7 @@ public Uni apply(AuthorizationCodeTokens session) { return authenticate(identityProviderManager, context, new IdTokenCredential(currentIdToken, isInternalIdToken(currentIdToken, configContext))) - .call(new Function>() { - @Override - public Uni apply(SecurityIdentity identity) { - if (isLogout(context, configContext)) { - LOG.debug("Performing an RP initiated logout"); - fireEvent(SecurityEvent.Type.OIDC_LOGOUT_RP_INITIATED, identity); - return buildLogoutRedirectUriUni(context, configContext, - session.getIdToken()); - } - if (isBackChannelLogoutPendingAndValid(configContext, identity) - || isFrontChannelLogoutValid(context, configContext, - identity)) { - return removeSessionCookie(context, configContext.oidcConfig) - .map(new Function() { - @Override - public Void apply(Void t) { - throw new LogoutException(); - } - }); - - } - return VOID_UNI; - } - }).onFailure() + .call(new LogoutCall(context, configContext, session.getIdToken())).onFailure() .recoverWithUni(new Function>() { @Override public Uni apply(Throwable t) { @@ -344,26 +321,35 @@ public Uni apply(Throwable t) { session.getRefreshToken(), context, identityProviderManager, false, null); - } else if (session.getRefreshToken() != null) { - // Token has nearly expired, try to refresh - LOG.debug("Token auto-refresh is starting"); - return refreshSecurityIdentity(configContext, - currentIdToken, - session.getRefreshToken(), - context, - identityProviderManager, true, - ((TokenAutoRefreshException) t).getSecurityIdentity()); } else { - LOG.debug( - "Token auto-refresh is required but is not possible because the refresh token is null"); - // Auto-refreshing is not possible, just continue with the current security identity + // Token auto-refresh, security identity is still valid SecurityIdentity currentIdentity = ((TokenAutoRefreshException) t) .getSecurityIdentity(); - if (currentIdentity != null) { - return Uni.createFrom().item(currentIdentity); + if (isLogout(context, configContext, currentIdentity)) { + // No need to refresh the token since the user is requesting a logout + return Uni.createFrom().item(currentIdentity).call( + new LogoutCall(context, configContext, session.getIdToken())); + } + + if (session.getRefreshToken() != null) { + // Token has nearly expired, try to refresh + LOG.debug("Token auto-refresh is starting"); + return refreshSecurityIdentity(configContext, + currentIdToken, + session.getRefreshToken(), + context, + identityProviderManager, true, + currentIdentity); } else { - return Uni.createFrom() - .failure(new AuthenticationFailedException(t.getCause())); + LOG.debug( + "Token auto-refresh is required but is not possible because the refresh token is null"); + // Auto-refreshing is not possible, just continue with the current security identity + if (currentIdentity != null) { + return Uni.createFrom().item(currentIdentity); + } else { + return Uni.createFrom() + .failure(new AuthenticationFailedException(t.getCause())); + } } } } @@ -388,8 +374,31 @@ private static String decryptIdTokenIfEncryptedByProvider(TenantConfigContext re return token; } - private boolean isBackChannelLogoutPendingAndValid(TenantConfigContext configContext, SecurityIdentity identity) { + private boolean isLogout(RoutingContext context, TenantConfigContext configContext, SecurityIdentity identity) { + return isRpInitiatedLogout(context, configContext) || isBackChannelLogoutPending(configContext, identity) + || isFrontChannelLogoutValid(context, configContext, identity); + } + + private boolean isBackChannelLogoutPending(TenantConfigContext configContext, SecurityIdentity identity) { + if (configContext.oidcConfig.logout.backchannel.path.isEmpty()) { + return false; + } + BackChannelLogoutTokenCache tokens = resolver.getBackChannelLogoutTokens() + .get(configContext.oidcConfig.getTenantId().get()); + if (tokens != null) { + JsonObject idTokenJson = OidcUtils.decodeJwtContent(((JsonWebToken) (identity.getPrincipal())).getRawToken()); + + String logoutTokenKeyValue = idTokenJson.getString(configContext.oidcConfig.logout.backchannel.getLogoutTokenKey()); + return tokens.containsTokenVerification(logoutTokenKeyValue); + } + return false; + } + + private boolean isBackChannelLogoutPendingAndValid(TenantConfigContext configContext, SecurityIdentity identity) { + if (configContext.oidcConfig.logout.backchannel.path.isEmpty()) { + return false; + } BackChannelLogoutTokenCache tokens = resolver.getBackChannelLogoutTokens() .get(configContext.oidcConfig.getTenantId().get()); if (tokens != null) { @@ -1014,7 +1023,7 @@ private String buildUri(RoutingContext context, boolean forceHttps, String autho .toString(); } - private boolean isLogout(RoutingContext context, TenantConfigContext configContext) { + private boolean isRpInitiatedLogout(RoutingContext context, TenantConfigContext configContext) { return isEqualToRequestPath(configContext.oidcConfig.logout.path, context, configContext); } @@ -1205,4 +1214,38 @@ static String getCookieSuffix(OidcTenantConfig oidcConfig) { ? (tenantIdSuffix + UNDERSCORE + oidcConfig.authentication.cookieSuffix.get()) : tenantIdSuffix; } + + private class LogoutCall implements Function> { + RoutingContext context; + TenantConfigContext configContext; + String idToken; + + LogoutCall(RoutingContext context, TenantConfigContext configContext, String idToken) { + this.context = context; + this.configContext = configContext; + this.idToken = idToken; + } + + @Override + public Uni apply(SecurityIdentity identity) { + if (isRpInitiatedLogout(context, configContext)) { + LOG.debug("Performing an RP initiated logout"); + fireEvent(SecurityEvent.Type.OIDC_LOGOUT_RP_INITIATED, identity); + return buildLogoutRedirectUriUni(context, configContext, idToken); + } + if (isBackChannelLogoutPendingAndValid(configContext, identity) + || isFrontChannelLogoutValid(context, configContext, + identity)) { + return removeSessionCookie(context, configContext.oidcConfig) + .map(new Function() { + @Override + public Void apply(Void t) { + throw new LogoutException(); + } + }); + + } + return VOID_UNI; + } + } } diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index bb6917d30bc2f..4e8bfb21b4c10 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -20,6 +20,8 @@ quarkus.oidc.code-flow.logout.extra-params.client_id=${quarkus.oidc.code-flow.cl quarkus.oidc.code-flow.credentials.secret=secret quarkus.oidc.code-flow.application-type=web-app quarkus.oidc.code-flow.token.audience=https://server.example.com +quarkus.oidc.code-flow.token.refresh-expired=true +quarkus.oidc.code-flow.token.refresh-token-time-skew=5M quarkus.oidc.code-flow-encrypted-id-token-jwk.auth-server-url=${keycloak.url}/realms/quarkus/ quarkus.oidc.code-flow-encrypted-id-token-jwk.client-id=quarkus-web-app diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java index 51e1b9a932d1f..472c2743bc4b6 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java @@ -6,7 +6,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.matching; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; -import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -77,7 +76,7 @@ public void testCodeFlow() throws IOException { assertEquals("alice, cache size: 0", page.getBody().asNormalizedText()); assertNotNull(getSessionCookie(webClient, "code-flow")); - + // Logout page = webClient.getPage("http://localhost:8081/code-flow/logout"); assertEquals("Welcome, clientId: quarkus-web-app", page.getBody().asNormalizedText()); assertNull(getSessionCookie(webClient, "code-flow"));