From 3d7db50fc1fef7f8b517ab1b6325f4b207613367 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Mon, 21 Oct 2024 16:27:15 +0100 Subject: [PATCH] Apply the required claims restriction to OIDC introspections --- .../io/quarkus/oidc/runtime/OidcProvider.java | 21 +++++++++++++ .../keycloak/CustomTenantConfigResolver.java | 3 ++ .../io/quarkus/it/keycloak/OidcResource.java | 31 +++++++++++++++++-- .../BearerTokenAuthorizationTest.java | 25 +++++++++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) 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 c315289615ffbd..613ab9410a60d2 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 @@ -378,6 +378,27 @@ public TokenIntrospection apply(TokenIntrospection introspectionResult, Throwabl throw new AuthenticationFailedException(ex); } + if (requiredClaims != null && !requiredClaims.isEmpty()) { + for (Map.Entry requiredClaim : requiredClaims.entrySet()) { + String introspectionClaimValue = null; + try { + introspectionClaimValue = introspectionResult.getString(requiredClaim.getKey()); + } catch (ClassCastException ex) { + LOG.debugf("Introspection claim %s is not String", requiredClaim.getKey()); + throw new AuthenticationFailedException(); + } + if (introspectionClaimValue == null) { + LOG.debugf("Introspection claim %s is missing", requiredClaim.getKey()); + throw new AuthenticationFailedException(); + } + if (!introspectionClaimValue.equals(requiredClaim.getValue())) { + LOG.debugf("Value of the introspection claim %s does not match required value of %s", + requiredClaim.getKey(), requiredClaim.getValue()); + throw new AuthenticationFailedException(); + } + } + } + return introspectionResult; } diff --git a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java index 8646416ae714b0..b76c13fb46652a 100644 --- a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java +++ b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java @@ -1,6 +1,7 @@ package io.quarkus.it.keycloak; import java.time.Duration; +import java.util.Map; import java.util.function.Supplier; import jakarta.enterprise.context.ApplicationScoped; @@ -52,6 +53,8 @@ public OidcTenantConfig get() { // authServerUri points to the JAX-RS `OidcResource`, root path is `/oidc` final String authServerUri; if (path.contains("tenant-opaque")) { + config.token.setRequiredClaims(Map.of("required_claim", "1")); + if (path.endsWith("/tenant-opaque/tenant-oidc/api/user")) { authServerUri = uri.replace("/tenant-opaque/tenant-oidc/api/user", "/oidc"); } else if (path.endsWith("/tenant-opaque/tenant-oidc/api/user-permission")) { diff --git a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/OidcResource.java b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/OidcResource.java index dc776517ea3f6a..a65841a7a7c51c 100644 --- a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/OidcResource.java +++ b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/OidcResource.java @@ -40,6 +40,7 @@ public class OidcResource { private volatile boolean rotate; private volatile int jwkEndpointCallCount; private volatile int introspectionEndpointCallCount; + private volatile int opaqueToken2UsageCount; private volatile int revokeEndpointCallCount; private volatile int userInfoEndpointCallCount; private volatile boolean enableDiscovery = true; @@ -112,6 +113,13 @@ public int resetIntrospectionEndpointCallCount() { return introspectionEndpointCallCount; } + @POST + @Path("opaque-token-call-count") + public int resetOpaqueTokenCallCount() { + opaqueToken2UsageCount = 0; + return opaqueToken2UsageCount; + } + @POST @Produces("application/json") @Path("introspect") @@ -120,7 +128,15 @@ public String introspect(@FormParam("client_id") String clientId, @FormParam("cl introspectionEndpointCallCount++; boolean activeStatus = introspection && !token.endsWith("-invalid"); - + boolean requiredClaim = true; + if (token.endsWith("_2")) { + opaqueToken2UsageCount++; + if (opaqueToken2UsageCount == 2) { + // This is to confirm that the same opaque token_2 works well when its introspection response + // includes `required_claim` with value "1" but fails when the required claim is not included + requiredClaim = false; + } + } String introspectionClientId = "none"; String introspectionClientSecret = "none"; if (clientSecret != null) { @@ -146,6 +162,7 @@ public String introspect(@FormParam("client_id") String clientId, @FormParam("cl " \"scope\": \"user\"," + " \"email\": \"user@gmail.com\"," + " \"username\": \"alice\"," + + (requiredClaim ? "\"required_claim\": \"1\"," : "") + " \"introspection_client_id\": \"" + introspectionClientId + "\"," + " \"introspection_client_secret\": \"" + introspectionClientSecret + "\"," + " \"client_id\": \"" + clientId + "\"" + @@ -251,13 +268,23 @@ public String testAccessTokenWithEmptyScope(@QueryParam("kid") String kid, @Quer @POST @Path("opaque-token") @Produces("application/json") - public String testOpaqueToken(@QueryParam("kid") String kid) { + public String testOpaqueToken() { return "{\"access_token\": \"987654321\"," + " \"token_type\": \"Bearer\"," + " \"refresh_token\": \"123456789\"," + " \"expires_in\": 300 }"; } + @POST + @Path("opaque-token2") + @Produces("application/json") + public String testOpaqueToken2() { + return "{\"access_token\": \"987654321_2\"," + + " \"token_type\": \"Bearer\"," + + " \"refresh_token\": \"123456789\"," + + " \"expires_in\": 300 }"; + } + @POST @Path("enable-introspection") public boolean setIntrospection() { diff --git a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java index a1e7e351e9e29a..fc2dff43acc226 100644 --- a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java +++ b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java @@ -723,6 +723,22 @@ public void testOpaqueTokenScopePermission() { .when().get("/tenant-opaque/tenant-oidc/api/admin-permission") .then() .statusCode(403); + + // Successful request with opaque token 2 + String opaqueToken2 = getOpaqueAccessToken2FromSimpleOidc(); + RestAssured.given().auth().oauth2(opaqueToken2) + .when().get("/tenant-opaque/tenant-oidc/api/user-permission") + .then() + .statusCode(200) + .body(equalTo("user")); + + // Expected to fail now because its introspection does not include the expected required claim + RestAssured.given().auth().oauth2(opaqueToken2) + .when().get("/tenant-opaque/tenant-oidc/api/user-permission") + .then() + .statusCode(401); + + RestAssured.when().post("/oidc/opaque-token-call-count").then().body(equalTo("0")); } @Test @@ -900,6 +916,15 @@ private String getOpaqueAccessTokenFromSimpleOidc() { return object.getString("access_token"); } + private String getOpaqueAccessToken2FromSimpleOidc() { + String json = RestAssured + .when() + .post("/oidc/opaque-token2") + .body().asString(); + JsonObject object = new JsonObject(json); + return object.getString("access_token"); + } + static WebClient createWebClient() { WebClient webClient = new WebClient(); webClient.setCssErrorHandler(new SilentCssErrorHandler());