From a8ad8d93635cfbf5cad5dc4bb66d627374d0f0e2 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Thu, 16 May 2024 14:45:42 +0100 Subject: [PATCH] Show how to handle multiple OIDC token audiences --- ...odeFlowVerifyIdAndAccessTokenResource.java | 40 +++++++++++++++++++ .../src/main/resources/application.properties | 12 +++++- .../keycloak/CodeFlowAuthorizationTest.java | 25 ++++++++++++ .../oidc/server/OidcWiremockTestResource.java | 5 ++- 4 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowVerifyIdAndAccessTokenResource.java diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowVerifyIdAndAccessTokenResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowVerifyIdAndAccessTokenResource.java new file mode 100644 index 0000000000000..b3ffecf4c6bcf --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowVerifyIdAndAccessTokenResource.java @@ -0,0 +1,40 @@ +package io.quarkus.it.keycloak; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.oidc.IdToken; +import io.quarkus.oidc.runtime.DefaultTokenIntrospectionUserInfoCache; +import io.quarkus.security.Authenticated; +import io.vertx.ext.web.RoutingContext; + +@Path("/code-flow-verify-id-and-access-tokens") +public class CodeFlowVerifyIdAndAccessTokenResource { + + @Inject + @IdToken + JsonWebToken idToken; + + @Inject + JsonWebToken accessToken; + + @Inject + RoutingContext routingContext; + + @Inject + DefaultTokenIntrospectionUserInfoCache tokenCache; + + @GET + @Authenticated + public String access() { + return "access token verified: " + (routingContext.get("code_flow_access_token_result") != null) + + ", id_token issuer: " + idToken.getIssuer() + + ", access_token issuer: " + accessToken.getIssuer() + + ", id_token audience: " + idToken.getAudience().iterator().next() + + ", access_token audience: " + accessToken.getAudience().iterator().next() + + ", cache size: " + tokenCache.getCacheSize(); + } +} diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index 15e351b94c6bf..a25886b891e32 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -24,10 +24,18 @@ quarkus.oidc.code-flow.logout.post-logout-uri-param=returnTo quarkus.oidc.code-flow.logout.extra-params.client_id=${quarkus.oidc.code-flow.client-id} 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.audience=https://id.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-verify-id-and-access-tokens.auth-server-url=${keycloak.url}/realms/quarkus/ +quarkus.oidc.code-flow-verify-id-and-access-tokens.client-id=quarkus-web-app +quarkus.oidc.code-flow-verify-id-and-access-tokens.authentication.user-info-required=false +quarkus.oidc.code-flow-verify-id-and-access-tokens.authentication.verify-access-token=true +quarkus.oidc.code-flow-verify-id-and-access-tokens.credentials.secret=secret +quarkus.oidc.code-flow-verify-id-and-access-tokens.application-type=web-app +quarkus.oidc.code-flow-verify-id-and-access-tokens.token.audience=any + 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 quarkus.oidc.code-flow-encrypted-id-token-jwk.credentials.secret=secret @@ -60,7 +68,7 @@ quarkus.oidc.code-flow-form-post.token-path=${keycloak.url}/realms/quarkus/token 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 quarkus.oidc.code-flow-form-post.logout.frontchannel.path=/code-flow-form-post/front-channel-logout -quarkus.oidc.code-flow-form-post.token.audience=https://server.example.com +quarkus.oidc.code-flow-form-post.token.audience=https://server.example.com,https://id.server.example.com quarkus.oidc.code-flow-user-info-only.auth-server-url=${keycloak.url}/realms/quarkus/ quarkus.oidc.code-flow-user-info-only.discovery-enabled=false 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 f41a520b1b9a2..b0c1533c81255 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 @@ -93,6 +93,31 @@ public void testCodeFlow() throws IOException { clearCache(); } + @Test + public void testCodeFlowVerifyIdAndAccessToken() throws IOException { + defineCodeFlowLogoutStub(); + try (final WebClient webClient = createWebClient()) { + webClient.getOptions().setRedirectEnabled(true); + HtmlPage page = webClient.getPage("http://localhost:8081/code-flow-verify-id-and-access-tokens"); + + HtmlForm form = page.getFormByName("form"); + form.getInputByName("username").type("alice"); + form.getInputByName("password").type("alice"); + + TextPage textPage = form.getInputByValue("login").click(); + + assertEquals("access token verified: true," + + " id_token issuer: https://server.example.com," + + " access_token issuer: https://server.example.com," + + " id_token audience: https://id.server.example.com," + + " access_token audience: https://server.example.com," + + " cache size: 0", textPage.getContent()); + assertNotNull(getSessionCookie(webClient, "code-flow-verify-id-and-access-tokens")); + webClient.getCookieManager().clearCookies(); + } + clearCache(); + } + @Test public void testCodeFlowEncryptedIdTokenJwk() throws IOException { doTestCodeFlowEncryptedIdToken("code-flow-encrypted-id-token-jwk", KeyEncryptionAlgorithm.DIR); diff --git a/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java b/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java index 9f76443d13692..bd7bd43903bfd 100644 --- a/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java +++ b/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java @@ -43,6 +43,8 @@ public class OidcWiremockTestResource implements QuarkusTestResourceLifecycleMan "https://server.example.com"); private static final String TOKEN_AUDIENCE = System.getProperty("quarkus.test.oidc.token.audience", "https://server.example.com"); + private static final String ID_TOKEN_AUDIENCE = System.getProperty("quarkus.test.oidc.idtoken.audience", + "https://id.server.example.com"); private static final String TOKEN_SUBJECT = "123456"; private static final String BEARER_TOKEN_TYPE = "Bearer"; private static final String ID_TOKEN_TYPE = "ID"; @@ -385,10 +387,11 @@ public static String generateJwtToken(String userName, Set groups, Strin } public static String generateJwtToken(String userName, Set groups, String sub, String type) { + final String audience = ID_TOKEN_TYPE.equals(type) ? ID_TOKEN_AUDIENCE : TOKEN_AUDIENCE; JwtClaimsBuilder builder = Jwt.preferredUserName(userName) .groups(groups) .issuer(TOKEN_ISSUER) - .audience(TOKEN_AUDIENCE) + .audience(audience) .claim("sid", "session-id") .subject(sub); if (type != null) {