diff --git a/docs/src/main/asciidoc/security-openid-connect-providers.adoc b/docs/src/main/asciidoc/security-openid-connect-providers.adoc index 0672a7b2896c5..668609f81c1a2 100644 --- a/docs/src/main/asciidoc/security-openid-connect-providers.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-providers.adoc @@ -49,6 +49,8 @@ quarkus.oidc.client-id= quarkus.oidc.credentials.secret= ---- +TIP: You can also use GitHub provider with `quarkus.oidc.application-type=service`, just set `quarkus.oidc.verify-access-token-with-user-info` configuration property to `true`. + === Google In order to set up OIDC for Google you need to create a new project in your https://console.cloud.google.com/projectcreate[Google Cloud Platform console]: diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/OpaqueTokenVerificationWithUserInfoValidationTest.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/OpaqueTokenVerificationWithUserInfoValidationTest.java new file mode 100644 index 0000000000000..b6eaff8715bec --- /dev/null +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/OpaqueTokenVerificationWithUserInfoValidationTest.java @@ -0,0 +1,44 @@ +package io.quarkus.oidc.test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; + +public class OpaqueTokenVerificationWithUserInfoValidationTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset("quarkus.oidc.token.verify-access-token-with-user-info=true\n"), + "application.properties")) + .assertException(t -> { + Throwable e = t; + ConfigurationException te = null; + while (e != null) { + if (e instanceof ConfigurationException) { + te = (ConfigurationException) e; + break; + } + e = e.getCause(); + } + assertNotNull(te); + // assert UserInfo is required + assertTrue( + te.getMessage() + .contains("UserInfo is not required but 'verifyAccessTokenWithUserInfo' is enabled"), + te.getMessage()); + }); + + @Test + public void test() { + Assertions.fail(); + } + +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java index b8a69106aea5d..15b68c43cf718 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java @@ -7,7 +7,6 @@ import java.util.Map; import java.util.Optional; import java.util.OptionalInt; -import java.util.Set; import io.quarkus.oidc.common.runtime.OidcCommonConfig; import io.quarkus.oidc.common.runtime.OidcConstants; @@ -1189,6 +1188,16 @@ public static Token fromAudience(String... audience) { @ConfigItem(defaultValue = "true") public boolean allowOpaqueTokenIntrospection = true; + /** + * Indirectly verify that the opaque (binary) access token is valid by using it to request UserInfo. + * Opaque access token is considered valid if the provider accepted this token and returned a valid UserInfo. + * You should only enable this option if the opaque access tokens have to be accepted but OpenId Connect + * provider does not have a token introspection endpoint. + * This property will have no effect when JWT tokens have to be verified. + */ + @ConfigItem(defaultValue = "false") + public boolean verifyAccessTokenWithUserInfo; + public Optional getIssuer() { return issuer; } 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 91a9ece130067..8f28ce75eee51 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 @@ -99,38 +99,40 @@ private Uni validateAllTokensWithOidcServer(RoutingContext ver TokenAuthenticationRequest request, TenantConfigContext resolvedContext) { - Uni codeAccessTokenUni = verifyCodeFlowAccessTokenUni(vertxContext, request, resolvedContext); + Uni userInfo = resolvedContext.oidcConfig.authentication.isUserInfoRequired().orElse(false) + ? getUserInfoUni(vertxContext, request, resolvedContext) + : NULL_USER_INFO_UNI; - return codeAccessTokenUni.onItemOrFailure().transformToUni( - new BiFunction>() { + return userInfo.onItemOrFailure().transformToUni( + new BiFunction>() { @Override - public Uni apply(TokenVerificationResult codeAccessToken, Throwable t) { + public Uni apply(UserInfo userInfo, Throwable t) { if (t != null) { return Uni.createFrom().failure(new AuthenticationFailedException(t)); } - return validateTokenWithOidcServer(vertxContext, request, resolvedContext, codeAccessToken); + return validateTokenWithOidcServer(vertxContext, request, resolvedContext, userInfo); } }); } private Uni validateTokenWithOidcServer(RoutingContext vertxContext, TokenAuthenticationRequest request, - TenantConfigContext resolvedContext, TokenVerificationResult codeAccessTokenResult) { + TenantConfigContext resolvedContext, UserInfo userInfo) { - if (codeAccessTokenResult != null) { - vertxContext.put(CODE_ACCESS_TOKEN_RESULT, codeAccessTokenResult); - } - - Uni userInfo = resolvedContext.oidcConfig.authentication.isUserInfoRequired().orElse(false) - ? getUserInfoUni(vertxContext, request, resolvedContext) - : NULL_USER_INFO_UNI; + Uni codeAccessTokenUni = verifyCodeFlowAccessTokenUni(vertxContext, request, resolvedContext, + userInfo); - return userInfo.onItemOrFailure().transformToUni( - new BiFunction>() { + return codeAccessTokenUni.onItemOrFailure().transformToUni( + new BiFunction>() { @Override - public Uni apply(UserInfo userInfo, Throwable t) { + public Uni apply(TokenVerificationResult codeAccessToken, Throwable t) { if (t != null) { return Uni.createFrom().failure(new AuthenticationFailedException(t)); } + + if (codeAccessToken != null) { + vertxContext.put(CODE_ACCESS_TOKEN_RESULT, codeAccessToken); + } + return createSecurityIdentityWithOidcServer(vertxContext, request, resolvedContext, userInfo); } }); @@ -148,7 +150,7 @@ private Uni createSecurityIdentityWithOidcServer(RoutingContex tokenUni = verifySelfSignedTokenUni(resolvedContext, request.getToken().getToken()); } } else { - tokenUni = verifyTokenUni(resolvedContext, request.getToken().getToken()); + tokenUni = verifyTokenUni(resolvedContext, request.getToken().getToken(), userInfo); } return tokenUni.onItemOrFailure() @@ -194,28 +196,40 @@ public Uni apply(TokenVerificationResult result, Throwable t) QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(); builder.addCredential(tokenCred); OidcUtils.setSecurityIdentityUserInfo(builder, userInfo); - OidcUtils.setSecurityIdentityIntrospection(builder, result.introspectionResult); OidcUtils.setSecurityIdentityConfigMetadata(builder, resolvedContext); - String principalMember = ""; - if (result.introspectionResult.contains(OidcConstants.INTROSPECTION_TOKEN_USERNAME)) { - principalMember = OidcConstants.INTROSPECTION_TOKEN_USERNAME; - } else if (result.introspectionResult.contains(OidcConstants.INTROSPECTION_TOKEN_SUB)) { - // fallback to "sub", if "username" is not present - principalMember = OidcConstants.INTROSPECTION_TOKEN_SUB; + final String userName; + if (result.introspectionResult == null) { + if (resolvedContext.oidcConfig.token.allowJwtIntrospection) { + userName = ""; + } else { + // we don't expect this to ever happen + LOG.debug("Illegal state - token introspection result is not available."); + return Uni.createFrom().failure(new AuthenticationFailedException()); + } + } else { + OidcUtils.setSecurityIdentityIntrospection(builder, result.introspectionResult); + String principalMember = ""; + if (result.introspectionResult.contains(OidcConstants.INTROSPECTION_TOKEN_USERNAME)) { + principalMember = OidcConstants.INTROSPECTION_TOKEN_USERNAME; + } else if (result.introspectionResult.contains(OidcConstants.INTROSPECTION_TOKEN_SUB)) { + // fallback to "sub", if "username" is not present + principalMember = OidcConstants.INTROSPECTION_TOKEN_SUB; + } + userName = principalMember.isEmpty() ? "" + : result.introspectionResult.getString(principalMember); + if (result.introspectionResult.contains(OidcConstants.TOKEN_SCOPE)) { + for (String role : result.introspectionResult.getString(OidcConstants.TOKEN_SCOPE) + .split(" ")) { + builder.addRole(role.trim()); + } + } } - final String userName = principalMember.isEmpty() ? "" - : result.introspectionResult.getString(principalMember); builder.setPrincipal(new Principal() { @Override public String getName() { return userName; } }); - if (result.introspectionResult.contains(OidcConstants.TOKEN_SCOPE)) { - for (String role : result.introspectionResult.getString(OidcConstants.TOKEN_SCOPE).split(" ")) { - builder.addRole(role.trim()); - } - } if (userInfo != null) { OidcUtils.setSecurityIdentityRoles(builder, resolvedContext.oidcConfig, new JsonObject(userInfo.getJsonObject().toString())); @@ -271,23 +285,34 @@ private static JsonObject getRolesJson(RoutingContext vertxContext, TenantConfig private Uni verifyCodeFlowAccessTokenUni(RoutingContext vertxContext, TokenAuthenticationRequest request, - TenantConfigContext resolvedContext) { + TenantConfigContext resolvedContext, UserInfo userInfo) { if (request.getToken() instanceof IdTokenCredential && (resolvedContext.oidcConfig.authentication.verifyAccessToken || resolvedContext.oidcConfig.roles.source.orElse(null) == Source.accesstoken)) { final String codeAccessToken = (String) vertxContext.get(OidcConstants.ACCESS_TOKEN_VALUE); - return verifyTokenUni(resolvedContext, codeAccessToken); + return verifyTokenUni(resolvedContext, codeAccessToken, userInfo); } else { return NULL_CODE_ACCESS_TOKEN_UNI; } } - private Uni verifyTokenUni(TenantConfigContext resolvedContext, String token) { + private Uni verifyTokenUni(TenantConfigContext resolvedContext, String token, UserInfo userInfo) { if (OidcUtils.isOpaqueToken(token)) { if (!resolvedContext.oidcConfig.token.allowOpaqueTokenIntrospection) { LOG.debug("Token is opaque but the opaque token introspection is not allowed"); throw new AuthenticationFailedException(); } + // verify opaque access token with UserInfo if enabled and introspection URI is absent + if (resolvedContext.oidcConfig.token.verifyAccessTokenWithUserInfo + && resolvedContext.provider.getMetadata().getIntrospectionUri() == null) { + if (userInfo == null) { + return Uni.createFrom().failure( + new AuthenticationFailedException("Opaque access token verification failed as user info is null.")); + } else { + // valid token verification result + return Uni.createFrom().item(new TokenVerificationResult(null, null)); + } + } LOG.debug("Starting the opaque token introspection"); return introspectTokenUni(resolvedContext, token); } else if (resolvedContext.provider.getMetadata().getJsonWebKeySetUri() == null 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 d0db470486f0d..1d809ccd5cb4b 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 @@ -214,6 +214,23 @@ private Uni createTenantContext(Vertx vertx, OidcTenantConf } } + if (oidcConfig.token.verifyAccessTokenWithUserInfo) { + if (!oidcConfig.authentication.isUserInfoRequired().orElse(false)) { + throw new ConfigurationException( + "UserInfo is not required but 'verifyAccessTokenWithUserInfo' is enabled"); + } + if (!oidcConfig.isDiscoveryEnabled().orElse(true)) { + if (oidcConfig.userInfoPath.isEmpty()) { + throw new ConfigurationException( + "UserInfo path is missing but 'verifyAccessTokenWithUserInfo' is enabled"); + } + if (oidcConfig.introspectionPath.isPresent()) { + throw new ConfigurationException( + "Introspection path is configured and 'verifyAccessTokenWithUserInfo' is enabled, these options are mutually exclusive"); + } + } + } + return createOidcProvider(oidcConfig, tlsConfig, vertx) .onItem().transform(p -> new TenantConfigContext(p, oidcConfig)); } diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java index 1353ab349ff36..ecd6d4478671d 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java @@ -1,5 +1,6 @@ package io.quarkus.it.keycloak; +import javax.annotation.security.PermitAll; import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; @@ -48,4 +49,11 @@ public String accessGitHubCachedInIdToken() { public String accessDynamicGitHub() { return access(); } + + @GET + @PermitAll + @Path("/clear-token-cache") + public void clearTokenCache() { + tokenCache.clearCache(); + } } diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java index 5271cc2bdccae..84eae694557c0 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java @@ -21,6 +21,7 @@ public Uni augment(SecurityIdentity identity, AuthenticationRe if (routingContext != null && (routingContext.normalizedPath().endsWith("code-flow-user-info-only") || routingContext.normalizedPath().endsWith("code-flow-user-info-github") + || routingContext.normalizedPath().endsWith("bearer-user-info-github-service") || routingContext.normalizedPath().endsWith("code-flow-user-info-dynamic-github") || routingContext.normalizedPath().endsWith("code-flow-user-info-github-cached-in-idtoken"))) { QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity); diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java index 1473b7fb802a9..61ee4ff44d326 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java @@ -32,6 +32,9 @@ public String resolve(RoutingContext context) { if (path.endsWith("code-flow-user-info-github")) { return "code-flow-user-info-github"; } + if (path.endsWith("bearer-user-info-github-service")) { + return "bearer-user-info-github-service"; + } if (path.endsWith("code-flow-user-info-github-cached-in-idtoken")) { return "code-flow-user-info-github-cached-in-idtoken"; } diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OpaqueGithubResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OpaqueGithubResource.java new file mode 100644 index 0000000000000..db55846de3e89 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OpaqueGithubResource.java @@ -0,0 +1,31 @@ +package io.quarkus.it.keycloak; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import io.quarkus.oidc.AccessTokenCredential; +import io.quarkus.oidc.UserInfo; +import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; + +@Authenticated +@Path("/bearer-user-info-github-service") +public class OpaqueGithubResource { + + @Inject + UserInfo userInfo; + + @Inject + SecurityIdentity identity; + + @Inject + AccessTokenCredential accessTokenCredential; + + @GET + public String access() { + return String.format("%s:%s:%s", identity.getPrincipal().getName(), userInfo.getString("preferred_username"), + accessTokenCredential.getToken()); + } + +} diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index ee4cc8c82d988..2722c5d273122 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -72,6 +72,14 @@ quarkus.oidc.code-flow-user-info-github.code-grant.headers.X-Custom=XCustomHeade quarkus.oidc.code-flow-user-info-github.client-id=quarkus-web-app quarkus.oidc.code-flow-user-info-github.credentials.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow +quarkus.oidc.bearer-user-info-github-service.provider=github +quarkus.oidc.bearer-user-info-github-service.token.verify-access-token-with-user-info=true +quarkus.oidc.bearer-user-info-github-service.application-type=service +quarkus.oidc.bearer-user-info-github-service.auth-server-url=${keycloak.url}/realms/quarkus/ +quarkus.oidc.bearer-user-info-github-service.user-info-path=github/userinfo +quarkus.oidc.bearer-user-info-github-service.client-id=quarkus-web-app +quarkus.oidc.bearer-user-info-github-service.credentials.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow + quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.provider=github quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.auth-server-url=${keycloak.url}/realms/quarkus/ quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.authorization-path=/ diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerOpaqueTokenAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerOpaqueTokenAuthorizationTest.java index 88fdac3650729..2c70886ec6fa4 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerOpaqueTokenAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerOpaqueTokenAuthorizationTest.java @@ -1,5 +1,9 @@ package io.quarkus.it.keycloak; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.matching; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static org.hamcrest.Matchers.equalTo; import java.util.Arrays; @@ -7,8 +11,12 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; +import com.github.tomakehurst.wiremock.WireMockServer; + +import io.quarkus.oidc.common.runtime.OidcConstants; 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; @@ -16,6 +24,9 @@ @QuarkusTestResource(OidcWiremockTestResource.class) public class BearerOpaqueTokenAuthorizationTest { + @OidcWireMock + WireMockServer wireMockServer; + @Test public void testSecureAccessSuccessPreferredUsername() { for (String username : Arrays.asList("alice", "admin")) { @@ -64,4 +75,54 @@ public void testExpiredBearerToken() { .statusCode(401); } + @Test + public void testGitHubBearerTokenSuccess() { + final String validToken = OidcConstants.BEARER_SCHEME + " ghu_XirRniLaPuW53pDylNnAPOPBm14taM0C9HP4"; + wireMockServer.stubFor( + get(urlEqualTo("/auth/realms/quarkus/github/userinfo")) + .withHeader("Authorization", matching(validToken)) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\n" + + " \"preferred_username\": \"alice\"" + + "}"))); + + RestAssured.given() + .header("Authorization", validToken) + .get("/bearer-user-info-github-service") + .then() + .statusCode(200) + .body(Matchers.equalTo("alice:alice:ghu_XirRniLaPuW53pDylNnAPOPBm14taM0C9HP4")); + } + + @Test + public void testGitHubBearerTokenUnauthorized() { + final String invalidToken = OidcConstants.BEARER_SCHEME + " Invalid"; + wireMockServer.stubFor( + get(urlEqualTo("/auth/realms/quarkus/github/userinfo")) + .withHeader("Authorization", matching(invalidToken)) + .willReturn(aResponse().withStatus(401))); + + RestAssured.given() + .header("Authorization", invalidToken) + .get("/bearer-user-info-github-service") + .then() + .statusCode(401); + } + + @Test + public void testGitHubBearerTokenNullUserInfo() { + final String validToken = OidcConstants.BEARER_SCHEME + " ghu_AAAAniLaPuW53pDylNnAPOPBm14ta7777777"; + wireMockServer.stubFor( + get(urlEqualTo("/auth/realms/quarkus/github/userinfo")) + .withHeader("Authorization", matching(validToken)) + .willReturn(aResponse().withStatus(200).withBody((String) null))); + + RestAssured.given() + .header("Authorization", validToken) + .get("/bearer-user-info-github-service") + .then() + .statusCode(401); + } + } 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 9b574aa049a34..a6034104808fb 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 @@ -15,6 +15,7 @@ import java.net.URL; import java.util.Set; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; @@ -44,6 +45,17 @@ public class CodeFlowAuthorizationTest { @OidcWireMock WireMockServer wireMockServer; + @BeforeAll + public static void clearCache() { + // clear token cache to make tests idempotent as we experienced failures + // on Windows when BearerTokenAuthorizationTest run before CodeFlowAuthorizationTest + RestAssured + .given() + .get("http://localhost:8081/clear-token-cache") + .then() + .statusCode(204); + } + @Test public void testCodeFlow() throws IOException { defineCodeFlowLogoutStub();