diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java index 8dc67f4f41e115..0ed3cb90235ea0 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java @@ -34,6 +34,7 @@ public final class OidcConstants { public static final String INTROSPECTION_TOKEN_ISS = "iss"; public static final String REVOCATION_TOKEN = "token"; + public static final String REVOCATION_TOKEN_TYPE_HINT = "token_type_hint"; public static final String PASSWORD_GRANT_USERNAME = "username"; public static final String PASSWORD_GRANT_PASSWORD = "password"; diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java index 04515540e12d38..92cb146319724d 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java @@ -74,7 +74,7 @@ import io.quarkus.oidc.runtime.Jose4jRecorder; import io.quarkus.oidc.runtime.OidcAuthenticationMechanism; import io.quarkus.oidc.runtime.OidcConfig; -import io.quarkus.oidc.runtime.OidcConfigurationMetadataProducer; +import io.quarkus.oidc.runtime.OidcConfigurationAndProviderProducer; import io.quarkus.oidc.runtime.OidcIdentityProvider; import io.quarkus.oidc.runtime.OidcJsonWebTokenProducer; import io.quarkus.oidc.runtime.OidcRecorder; @@ -174,7 +174,7 @@ public void additionalBeans(BuildProducer additionalBea builder.addBeanClass(OidcAuthenticationMechanism.class) .addBeanClass(OidcJsonWebTokenProducer.class) .addBeanClass(OidcTokenCredentialProducer.class) - .addBeanClass(OidcConfigurationMetadataProducer.class) + .addBeanClass(OidcConfigurationAndProviderProducer.class) .addBeanClass(OidcIdentityProvider.class) .addBeanClass(DefaultTenantConfigResolver.class) .addBeanClass(DefaultTokenStateManager.class) diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcConfigurationMetadata.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcConfigurationMetadata.java index bcdde90ad1f74a..450b72da2cfa4e 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcConfigurationMetadata.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcConfigurationMetadata.java @@ -16,6 +16,7 @@ public class OidcConfigurationMetadata { public static final String USERINFO_ENDPOINT = "userinfo_endpoint"; public static final String END_SESSION_ENDPOINT = "end_session_endpoint"; private static final String REGISTRATION_ENDPOINT = "registration_endpoint"; + private static final String REVOCATION_ENDPOINT = "revocation_endpoint"; public static final String SCOPES_SUPPORTED = "scopes_supported"; private final String discoveryUri; @@ -26,6 +27,7 @@ public class OidcConfigurationMetadata { private final String userInfoUri; private final String endSessionUri; private final String registrationUri; + private final String revocationUri; private final String issuer; private final JsonObject json; @@ -36,6 +38,7 @@ public OidcConfigurationMetadata(String tokenUri, String userInfoUri, String endSessionUri, String registrationUri, + String revocationUri, String issuer) { this.discoveryUri = null; this.tokenUri = tokenUri; @@ -45,6 +48,7 @@ public OidcConfigurationMetadata(String tokenUri, this.userInfoUri = userInfoUri; this.endSessionUri = endSessionUri; this.registrationUri = registrationUri; + this.revocationUri = revocationUri; this.issuer = issuer; this.json = null; } @@ -70,6 +74,8 @@ public OidcConfigurationMetadata(JsonObject wellKnownConfig, OidcConfigurationMe localMetadataConfig == null ? null : localMetadataConfig.endSessionUri); this.registrationUri = getMetadataValue(wellKnownConfig, REGISTRATION_ENDPOINT, localMetadataConfig == null ? null : localMetadataConfig.registrationUri); + this.revocationUri = getMetadataValue(wellKnownConfig, REVOCATION_ENDPOINT, + localMetadataConfig == null ? null : localMetadataConfig.revocationUri); this.issuer = getMetadataValue(wellKnownConfig, ISSUER, localMetadataConfig == null ? null : localMetadataConfig.issuer); this.json = wellKnownConfig; @@ -87,6 +93,10 @@ public String getTokenUri() { return tokenUri; } + public String getRevocationUri() { + return revocationUri; + } + public String getIntrospectionUri() { return introspectionUri; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfigurationAndProviderProducer.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfigurationAndProviderProducer.java new file mode 100644 index 00000000000000..11dddc315d512a --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfigurationAndProviderProducer.java @@ -0,0 +1,54 @@ +package io.quarkus.oidc.runtime; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; + +import io.quarkus.oidc.OIDCException; +import io.quarkus.oidc.OidcConfigurationMetadata; +import io.quarkus.security.identity.SecurityIdentity; + +@RequestScoped +public class OidcConfigurationAndProviderProducer { + @Inject + TenantConfigBean tenantConfig; + @Inject + SecurityIdentity identity; + + @Produces + @RequestScoped + OidcConfigurationMetadata produceMetadata() { + OidcConfigurationMetadata configMetadata = OidcUtils.getAttribute(identity, OidcUtils.CONFIG_METADATA_ATTRIBUTE); + + if (configMetadata == null && tenantConfig.getDefaultTenant().oidcConfig().tenantEnabled()) { + configMetadata = tenantConfig.getDefaultTenant().provider().getMetadata(); + } + if (configMetadata == null) { + throw new OIDCException("OidcConfigurationMetadata can not be injected"); + } + return configMetadata; + } + + @Produces + @RequestScoped + OidcProviderClient produceProviderClient() { + OidcProviderClient client = null; + String tenantId = OidcUtils.getAttribute(identity, OidcUtils.TENANT_ID_ATTRIBUTE); + if (tenantId != null) { + if (OidcUtils.DEFAULT_TENANT_ID.equals(tenantId)) { + return tenantConfig.getDefaultTenant().getOidcProviderClient(); + } + TenantConfigContext context = tenantConfig.getStaticTenant(tenantId); + if (context == null) { + context = tenantConfig.getDynamicTenant(tenantId); + } + if (context != null) { + client = context.getOidcProviderClient(); + } + } + if (client == null) { + throw new OIDCException("OidcProviderClient can not be injected"); + } + return client; + } +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfigurationMetadataProducer.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfigurationMetadataProducer.java deleted file mode 100644 index e5f9eeab9ddf70..00000000000000 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfigurationMetadataProducer.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.quarkus.oidc.runtime; - -import jakarta.enterprise.context.RequestScoped; -import jakarta.enterprise.inject.Produces; -import jakarta.inject.Inject; - -import io.quarkus.oidc.OIDCException; -import io.quarkus.oidc.OidcConfigurationMetadata; -import io.quarkus.security.identity.SecurityIdentity; - -@RequestScoped -public class OidcConfigurationMetadataProducer { - @Inject - TenantConfigBean tenantConfig; - @Inject - SecurityIdentity identity; - - @Produces - @RequestScoped - OidcConfigurationMetadata produce() { - OidcConfigurationMetadata configMetadata = OidcUtils.getAttribute(identity, OidcUtils.CONFIG_METADATA_ATTRIBUTE); - - if (configMetadata == null && tenantConfig.getDefaultTenant().oidcConfig().tenantEnabled()) { - configMetadata = tenantConfig.getDefaultTenant().provider().getMetadata(); - } - if (configMetadata == null) { - throw new OIDCException("OidcConfigurationMetadata can not be injected"); - } - return configMetadata; - } -} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java index 717fa6b0cff89c..0ac608071e58ce 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java @@ -366,6 +366,7 @@ public String getName() { var vertxContext = getRoutingContextAttribute(request); OidcUtils.setBlockingApiAttribute(builder, vertxContext); OidcUtils.setRoutingContextAttribute(builder, vertxContext); + OidcUtils.setOidcProviderClientAttribute(builder, resolvedContext.getOidcProviderClient()); SecurityIdentity identity = builder.build(); // If the primary token is a bearer access token then there's no point of checking if // it should be refreshed as RT is only available for the code flow tokens diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java index a7f24b28a1740b..af60a1d552dac7 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java @@ -421,8 +421,8 @@ private boolean isTokenExpired(Long exp) { } private int getLifespanGrace() { - return client.getOidcConfig().token().lifespanGrace().isPresent() - ? client.getOidcConfig().token().lifespanGrace().getAsInt() + return oidcConfig.token().lifespanGrace().isPresent() + ? oidcConfig.token().lifespanGrace().getAsInt() : 0; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java index d1edf36eb1e72e..a11a3d30a270b1 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java @@ -84,7 +84,7 @@ private static String initIntrospectionBasicAuthScheme(OidcTenantConfig oidcConf } } - public OidcConfigurationMetadata getMetadata() { + OidcConfigurationMetadata getMetadata() { return metadata; } @@ -116,18 +116,18 @@ private Uni doGetJsonWebKeySet(OidcRequestContextProperties reque .transform(resp -> getJsonWebKeySet(requestProps, resp)); } - public Uni getUserInfo(final String token) { + public Uni getUserInfo(final String accessToken) { final OidcRequestContextProperties requestProps = getRequestProps(null, null); - return doGetUserInfo(requestProps, token, List.of()) + return doGetUserInfo(requestProps, accessToken, List.of()) .onFailure(OidcCommonUtils.validOidcClientRedirect(metadata.getUserInfoUri())) .recoverWithUni( new Function>() { @Override public Uni apply(Throwable t) { OidcClientRedirectException ex = (OidcClientRedirectException) t; - return doGetUserInfo(requestProps, token, ex.getCookies()); + return doGetUserInfo(requestProps, accessToken, ex.getCookies()); } }); } @@ -160,10 +160,6 @@ private JsonWebKeySet getJsonWebKeySet(OidcRequestContextProperties requestProps return new JsonWebKeySet(getString(requestProps, metadata.getJsonWebKeySetUri(), resp, OidcEndpoint.Type.JWKS)); } - public OidcTenantConfig getOidcConfig() { - return oidcConfig; - } - public Uni getAuthorizationCodeTokens(String code, String redirectUri, String codeVerifier) { final MultiMap codeGrantParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap()); codeGrantParams.add(OidcConstants.GRANT_TYPE, OidcConstants.AUTHORIZATION_CODE); @@ -189,6 +185,41 @@ public Uni refreshAuthorizationCodeTokens(String refres .transform(resp -> getAuthorizationCodeTokens(requestProps, resp)); } + public Uni revokeAccessToken(String accessToken) { + return revokeToken(accessToken, OidcConstants.ACCESS_TOKEN_VALUE); + } + + public Uni revokeRefreshToken(String refreshToken) { + return revokeToken(refreshToken, OidcConstants.REFRESH_TOKEN_VALUE); + } + + private Uni revokeToken(String token, String tokenTypeHint) { + + if (metadata.getRevocationUri() != null) { + OidcRequestContextProperties requestProps = getRequestProps(null, null); + MultiMap tokenRevokeParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap()); + tokenRevokeParams.set(OidcConstants.REVOCATION_TOKEN, token); + tokenRevokeParams.set(OidcConstants.REVOCATION_TOKEN_TYPE_HINT, tokenTypeHint); + + return getHttpResponse(requestProps, metadata.getRevocationUri(), tokenRevokeParams, false) + .transform(resp -> toRevokeResponse(requestProps, resp)); + } else { + LOG.debugf("The %s token can not be revoked because the revocation endpoint URL is not set", tokenTypeHint); + return Uni.createFrom().item(false); + } + + } + + private Boolean toRevokeResponse(OidcRequestContextProperties requestProps, HttpResponse resp) { + // Per RFC7009, 200 is returned if a token has been revoked successfully or if the client submitted an + // invalid token, https://datatracker.ietf.org/doc/html/rfc7009#section-2.2. + // 503 is at least theoretically possible if the OIDC server declines and suggests to Retry-After some period of time. + // However this period of time can be set to unpredictable value. + Buffer buffer = resp.body(); + OidcCommonUtils.filterHttpResponse(requestProps, resp, buffer, responseFilters, OidcEndpoint.Type.TOKEN_REVOCATION); + return resp.statusCode() == 503 ? false : true; + } + private UniOnItem> getHttpResponse(OidcRequestContextProperties requestProps, String uri, MultiMap formBody, boolean introspect) { HttpRequest request = client.postAbs(uri); @@ -320,7 +351,7 @@ public void close() { client.close(); } - public Key getClientJwtKey() { + Key getClientJwtKey() { return clientJwtKey; } @@ -357,15 +388,15 @@ private OidcRequestContextProperties getRequestProps(OidcRequestContextPropertie return new OidcRequestContextProperties(newProperties); } - public Vertx getVertx() { + Vertx getVertx() { return vertx; } - public WebClient getWebClient() { + WebClient getWebClient() { return client; } - static record UserInfoResponse(String contentType, String data) { + public static record UserInfoResponse(String contentType, String data) { }; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java index a2803969505bfd..15d7dfb311bb3a 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java @@ -615,9 +615,10 @@ private static OidcConfigurationMetadata createLocalMetadata(OidcTenantConfig oi String jwksUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.jwksPath()); String userInfoUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.userInfoPath()); String endSessionUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.endSessionPath()); - String registrationUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.registrationPath()); + String registrationUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.revokePath()); + String revocationUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.revokePath()); return new OidcConfigurationMetadata(tokenUri, - introspectionUri, authorizationUri, jwksUri, userInfoUri, endSessionUri, registrationUri, + introspectionUri, authorizationUri, jwksUri, userInfoUri, endSessionUri, registrationUri, revocationUri, oidcConfig.token().issuer().orElse(null)); } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java index 41538e753c74dc..4337b99dd57752 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java @@ -371,6 +371,7 @@ static QuarkusSecurityIdentity validateAndCreateIdentity(Map req builder.setPrincipal(jwtPrincipal); var vertxContext = getRoutingContextAttribute(request); setRoutingContextAttribute(builder, vertxContext); + OidcUtils.setOidcProviderClientAttribute(builder, resolvedContext.getOidcProviderClient()); setSecurityIdentityRoles(builder, config, rolesJson); setSecurityIdentityPermissions(builder, config, rolesJson); setSecurityIdentityUserInfo(builder, userInfo); @@ -419,6 +420,11 @@ public static void setRoutingContextAttribute(QuarkusSecurityIdentity.Builder bu builder.addAttribute(RoutingContext.class.getName(), routingContext); } + public static void setOidcProviderClientAttribute(QuarkusSecurityIdentity.Builder builder, + OidcProviderClient oidcProviderClient) { + builder.addAttribute(OidcProviderClient.class.getName(), oidcProviderClient); + } + public static void setSecurityIdentityUserInfo(QuarkusSecurityIdentity.Builder builder, UserInfo userInfo) { if (userInfo != null) { builder.addAttribute(USER_INFO_ATTRIBUTE, userInfo); diff --git a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java index 87ac6edf3bb4df..da6bcb0b60a196 100644 --- a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java +++ b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java @@ -22,6 +22,7 @@ import io.quarkus.oidc.RefreshToken; import io.quarkus.oidc.UserInfo; import io.quarkus.oidc.common.runtime.OidcConstants; +import io.quarkus.oidc.runtime.OidcProviderClient; import io.quarkus.security.Authenticated; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.runtime.SecurityIdentityAssociation; @@ -56,6 +57,9 @@ public class ProtectedResource { @Inject AccessTokenCredential accessTokenCredential; + @Inject + OidcProviderClient oidcProviderClient; + @Inject RefreshToken refreshToken; @@ -189,7 +193,13 @@ public String getAccessToken() { throw new OIDCException("Access token values are not equal"); } - return accessToken.getRawToken() != null && !accessToken.getRawToken().isEmpty() ? "AT injected" : "no access"; + return accessToken.getRawToken() != null && !accessToken.getRawToken().isEmpty() + ? "AT injected, active: " + isTokenActive() + : "no access"; + } + + private boolean isTokenActive() { + return oidcProviderClient.introspectToken(accessTokenCredential.getToken()).await().indefinitely().isActive(); } @GET 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 86a2689a2db340..2c48fc28acb122 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 @@ -1077,7 +1077,7 @@ public void testAccessTokenInjection() throws IOException { page = webClient.getPage("http://localhost:8081/web-app/access"); - assertEquals("AT injected", page.getBody().asNormalizedText()); + assertEquals("AT injected, active: true", page.getBody().asNormalizedText()); webClient.getCookieManager().clearCookies(); } } @@ -1240,7 +1240,7 @@ public void testDefaultSessionManagerSplitTokens() throws IOException, Interrupt page.getBody().asNormalizedText()); page = webClient.getPage("http://localhost:8081/web-app/access/tenant-split-tokens"); - assertEquals("tenant-split-tokens:AT injected", page.getBody().asNormalizedText()); + assertEquals("tenant-split-tokens:AT injected, active: true", page.getBody().asNormalizedText()); page = webClient.getPage("http://localhost:8081/web-app/refresh/tenant-split-tokens"); assertEquals("tenant-split-tokens:RT injected", page.getBody().asNormalizedText()); diff --git a/integration-tests/oidc-wiremock-logout/pom.xml b/integration-tests/oidc-wiremock-logout/pom.xml index 151ad1ec97221d..546a318c317c7f 100644 --- a/integration-tests/oidc-wiremock-logout/pom.xml +++ b/integration-tests/oidc-wiremock-logout/pom.xml @@ -39,6 +39,11 @@ rest-assured test + + org.awaitility + awaitility + test + org.htmlunit htmlunit diff --git a/integration-tests/oidc-wiremock-logout/src/main/java/io/quarkus/it/keycloak/SecurityEventListener.java b/integration-tests/oidc-wiremock-logout/src/main/java/io/quarkus/it/keycloak/SecurityEventListener.java new file mode 100644 index 00000000000000..850f58868eaf47 --- /dev/null +++ b/integration-tests/oidc-wiremock-logout/src/main/java/io/quarkus/it/keycloak/SecurityEventListener.java @@ -0,0 +1,24 @@ +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.ObservesAsync; + +import io.quarkus.oidc.AccessTokenCredential; +import io.quarkus.oidc.RefreshToken; +import io.quarkus.oidc.SecurityEvent; +import io.quarkus.oidc.runtime.OidcProviderClient; + +@ApplicationScoped +public class SecurityEventListener { + + public void event(@ObservesAsync SecurityEvent event) { + if (SecurityEvent.Type.OIDC_BACKCHANNEL_LOGOUT_COMPLETED == event.getEventType()) { + OidcProviderClient oidcProvider = event.getSecurityIdentity().getAttribute(OidcProviderClient.class.getName()); + String accessToken = event.getSecurityIdentity().getCredential(AccessTokenCredential.class).getToken(); + String refreshToken = event.getSecurityIdentity().getCredential(RefreshToken.class).getToken(); + oidcProvider.revokeAccessToken(accessToken).await().indefinitely(); + oidcProvider.revokeRefreshToken(refreshToken).await().indefinitely(); + } + } + +} diff --git a/integration-tests/oidc-wiremock-logout/src/main/resources/application.properties b/integration-tests/oidc-wiremock-logout/src/main/resources/application.properties index beaa3fa7f635e8..5690b3fc83c333 100644 --- a/integration-tests/oidc-wiremock-logout/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock-logout/src/main/resources/application.properties @@ -10,6 +10,7 @@ quarkus.oidc.code-flow-form-post.discovery-enabled=false quarkus.oidc.code-flow-form-post.authorization-path=/ # reuse the wiremock access token stub for the `quarkus` realm - it is the same for the query and form post response mode quarkus.oidc.code-flow-form-post.token-path=${keycloak.url}/realms/quarkus/token +quarkus.oidc.code-flow-form-post.revoke-path=${keycloak.url}/realms/quarkus/revoke # reuse the wiremock JWK endpoint stub for the `quarkus` realm - it is the same for the query and form post response mode quarkus.oidc.code-flow-form-post.jwks-path=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs quarkus.oidc.code-flow-form-post.logout.backchannel.path=/back-channel-logout @@ -17,4 +18,4 @@ quarkus.oidc.code-flow-form-post.token.audience=https://server.example.com,https quarkus.native.additional-build-args=-H:IncludeResources=private.*\\.*,-H:IncludeResources=.*\\.p12 -quarkus.http.root-path=/service \ No newline at end of file +quarkus.http.root-path=/service diff --git a/integration-tests/oidc-wiremock-logout/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java b/integration-tests/oidc-wiremock-logout/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java index 57201376da7b7e..27d8bea4beff12 100644 --- a/integration-tests/oidc-wiremock-logout/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java +++ b/integration-tests/oidc-wiremock-logout/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java @@ -1,11 +1,17 @@ package io.quarkus.it.keycloak; +import static com.github.tomakehurst.wiremock.client.WireMock.containing; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; +import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -import java.io.IOException; import java.net.URI; +import java.time.Duration; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; import org.htmlunit.SilentCssErrorHandler; import org.htmlunit.TextPage; @@ -17,8 +23,12 @@ import org.htmlunit.util.Cookie; import org.junit.jupiter.api.Test; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; + import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.oidc.server.OidcWireMock; import io.quarkus.test.oidc.server.OidcWiremockTestResource; import io.restassured.RestAssured; import io.restassured.http.ContentType; @@ -27,8 +37,12 @@ @QuarkusTestResource(OidcWiremockTestResource.class) public class CodeFlowAuthorizationTest { + @OidcWireMock + WireMockServer wireMockServer; + @Test - public void testCodeFlowFormPostAndBackChannelLogout() throws IOException { + public void testCodeFlowFormPostAndBackChannelLogout() throws Exception { + defineRevokeTokenStubs(); try (final WebClient webClient = createWebClient()) { webClient.getOptions().setRedirectEnabled(true); HtmlPage page = webClient.getPage("http://localhost:8081/service/code-flow-form-post"); @@ -64,6 +78,9 @@ public void testCodeFlowFormPostAndBackChannelLogout() throws IOException { // Session is still active assertNotNull(getSessionCookie(webClient, "code-flow-form-post")); + wireMockServer.verify(0, + postRequestedFor(urlPathMatching("/auth/realms/quarkus/revoke"))); + // request a back channel logout for the same subject RestAssured.given() .when().contentType(ContentType.URLENC).body("logout_token=" @@ -80,10 +97,44 @@ public void testCodeFlowFormPostAndBackChannelLogout() throws IOException { assertNull(getSessionCookie(webClient, "code-flow-form-post")); + await().atMost(10, TimeUnit.SECONDS) + .pollInterval(Duration.ofSeconds(3)) + .until(new Callable() { + @Override + public Boolean call() throws Exception { + try { + wireMockServer.verify(2, + postRequestedFor(urlPathMatching("/auth/realms/quarkus/revoke"))); + return true; + } catch (Throwable t) { + return false; + } + } + }); + + wireMockServer.verify(2, + postRequestedFor(urlPathMatching("/auth/realms/quarkus/revoke"))); + wireMockServer.resetRequests(); + webClient.getCookieManager().clearCookies(); } } + private void defineRevokeTokenStubs() { + wireMockServer + .stubFor(WireMock.post("/auth/realms/quarkus/revoke") + .withRequestBody(containing("token")) + .withRequestBody(containing("token_type_hint=access_token")) + .willReturn(WireMock.aResponse() + .withStatus(200))); + wireMockServer + .stubFor(WireMock.post("/auth/realms/quarkus/revoke") + .withRequestBody(containing("token")) + .withRequestBody(containing("token_type_hint=refresh_token")) + .willReturn(WireMock.aResponse() + .withStatus(200))); + } + private WebClient createWebClient() { WebClient webClient = new WebClient(); webClient.setCssErrorHandler(new SilentCssErrorHandler());