From 9c74b2c2b3ce88355dd62b1e95b44ab85b8a1f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Sat, 30 Mar 2024 15:17:28 +0100 Subject: [PATCH] Support for verifying OIDC JWT claims with custom Jose4j Validator --- ...rity-oidc-bearer-token-authentication.adoc | 33 +++++++ .../io/quarkus/oidc/runtime/OidcProvider.java | 44 ++++++++- .../oidc/runtime/OidcProviderTest.java | 92 +++++++++++++++++-- .../GlobalJwtPreferredNameValidator.java | 24 +++++ .../quarkus/it/keycloak/IssuerValidator.java | 28 ++++++ ...nantSpecificJwtPreferredNameValidator.java | 36 ++++++++ .../src/main/resources/application.properties | 5 + .../BearerTokenAuthorizationTest.java | 33 +++++++ .../it/keycloak/ServicePublicKeyTestCase.java | 14 +++ 9 files changed, 297 insertions(+), 12 deletions(-) create mode 100644 integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/GlobalJwtPreferredNameValidator.java create mode 100644 integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/IssuerValidator.java create mode 100644 integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantSpecificJwtPreferredNameValidator.java diff --git a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc index 175926e088469..b8ff578d99677 100644 --- a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc @@ -352,6 +352,39 @@ public class IssuerValidator implements ContainerRequestFilter { Consider using the `quarkus.oidc.token.audience` property to verify the token `aud` (`audience`) claim value. ==== +Sometimes, it's helpful to register your own JWT token's validator like in the example below: + +[source, java] +---- +package org.acme.security.openid.connect; + +import static org.eclipse.microprofile.jwt.Claims.iss; + +import io.quarkus.arc.Unremovable; +import jakarta.enterprise.context.ApplicationScoped; + +import org.jose4j.jwt.MalformedClaimException; +import org.jose4j.jwt.consumer.JwtContext; +import org.jose4j.jwt.consumer.Validator; + +@Unremovable +@ApplicationScoped +public class IssuerValidator implements Validator { <1> + + @Override + public String validate(JwtContext jwtContext) throws MalformedClaimException { + if (jwtContext.getJwtClaims().hasClaim(iss.name()) + && "my-issuer".equals(jwtContext.getJwtClaims().getClaimValueAsString(iss.name()))) { + return "wrong issuer"; + } + return null; + } +} +---- +<1> Register JWT token's validator for every OIDC tenant. + +TIP: Annotate the validator with a `@TenantFeature` annotation to make this validator tenant-specific. + [[single-page-applications]] === Single-page applications 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 fbc6dbbf54132..00e9d3c25791d 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 @@ -4,12 +4,14 @@ import java.nio.charset.StandardCharsets; import java.security.Key; import java.time.Duration; +import java.util.ArrayList; import java.util.Base64; import java.util.List; import java.util.Map; import java.util.function.BiFunction; import java.util.function.Function; +import jakarta.enterprise.inject.Default; import jakarta.json.JsonObject; import org.eclipse.microprofile.jwt.Claims; @@ -30,12 +32,14 @@ import org.jose4j.lang.InvalidAlgorithmException; import org.jose4j.lang.UnresolvableKeyException; +import io.quarkus.arc.Arc; import io.quarkus.logging.Log; import io.quarkus.oidc.AuthorizationCodeTokens; import io.quarkus.oidc.OIDCException; import io.quarkus.oidc.OidcConfigurationMetadata; import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.OidcTenantConfig.CertificateChain; +import io.quarkus.oidc.TenantFeature.TenantFeatureLiteral; import io.quarkus.oidc.TokenCustomizer; import io.quarkus.oidc.TokenIntrospection; import io.quarkus.oidc.UserInfo; @@ -67,6 +71,7 @@ public class OidcProvider implements Closeable { private static final AlgorithmConstraints SYMMETRIC_ALGORITHM_CONSTRAINTS = new AlgorithmConstraints( AlgorithmConstraints.ConstraintType.PERMIT, SignatureAlgorithm.HS256.getAlgorithm()); + private final List customValidators; final OidcProviderClient client; final RefreshableVerificationKeyResolver asymmetricKeyResolver; final DynamicVerificationKeyResolver keyResolverProvider; @@ -79,11 +84,12 @@ public class OidcProvider implements Closeable { final AlgorithmConstraints requiredAlgorithmConstraints; public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, JsonWebKeySet jwks, Key tokenDecryptionKey) { - this(client, oidcConfig, jwks, TokenCustomizerFinder.find(oidcConfig), tokenDecryptionKey); + this(client, oidcConfig, jwks, TokenCustomizerFinder.find(oidcConfig), tokenDecryptionKey, + getCustomValidators(oidcConfig)); } public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, JsonWebKeySet jwks, - TokenCustomizer tokenCustomizer, Key tokenDecryptionKey) { + TokenCustomizer tokenCustomizer, Key tokenDecryptionKey, List customValidators) { this.client = client; this.oidcConfig = oidcConfig; this.tokenCustomizer = tokenCustomizer; @@ -106,6 +112,12 @@ public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, Json this.requiredClaims = checkRequiredClaimsProp(); this.tokenDecryptionKey = tokenDecryptionKey; this.requiredAlgorithmConstraints = checkSignatureAlgorithm(); + + if (customValidators != null && !customValidators.isEmpty()) { + this.customValidators = customValidators; + } else { + this.customValidators = null; + } } public OidcProvider(String publicKeyEnc, OidcTenantConfig oidcConfig, Key tokenDecryptionKey) { @@ -125,6 +137,7 @@ public OidcProvider(String publicKeyEnc, OidcTenantConfig oidcConfig, Key tokenD this.requiredClaims = checkRequiredClaimsProp(); this.tokenDecryptionKey = tokenDecryptionKey; this.requiredAlgorithmConstraints = checkSignatureAlgorithm(); + this.customValidators = getCustomValidators(oidcConfig); } private AlgorithmConstraints checkSignatureAlgorithm() { @@ -210,6 +223,12 @@ private TokenVerificationResult verifyJwtTokenInternal(String token, builder.registerValidator(new CustomClaimsValidator(Map.of(OidcConstants.NONCE, nonce))); } + if (customValidators != null) { + for (Validator customValidator : customValidators) { + builder.registerValidator(customValidator); + } + } + if (issuedAtRequired) { builder.setRequireIssuedAt(); } @@ -598,4 +617,25 @@ public String validate(JwtContext jwtContext) throws MalformedClaimException { return null; } } + + private static List getCustomValidators(OidcTenantConfig oidcTenantConfig) { + if (oidcTenantConfig != null && oidcTenantConfig.tenantId.isPresent()) { + var tenantsValidators = new ArrayList(); + for (var instance : Arc.container().listAll(Validator.class, Default.Literal.INSTANCE)) { + if (instance.isAvailable()) { + tenantsValidators.add(instance.get()); + } + } + for (var instance : Arc.container().listAll(Validator.class, + TenantFeatureLiteral.of(oidcTenantConfig.tenantId.get()))) { + if (instance.isAvailable()) { + tenantsValidators.add(instance.get()); + } + } + if (!tenantsValidators.isEmpty()) { + return List.copyOf(tenantsValidators); + } + } + return null; + } } diff --git a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcProviderTest.java b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcProviderTest.java index cf8bd8babbefd..fe13364f4e63b 100644 --- a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcProviderTest.java +++ b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcProviderTest.java @@ -8,6 +8,7 @@ import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Base64; +import java.util.List; import jakarta.json.Json; import jakarta.json.JsonObject; @@ -19,7 +20,10 @@ import org.jose4j.jwk.RsaJwkGenerator; import org.jose4j.jws.AlgorithmIdentifiers; import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwt.MalformedClaimException; import org.jose4j.jwt.consumer.InvalidJwtException; +import org.jose4j.jwt.consumer.JwtContext; +import org.jose4j.jwt.consumer.Validator; import org.jose4j.keys.EllipticCurves; import org.jose4j.lang.UnresolvableKeyException; import org.junit.jupiter.api.Test; @@ -41,7 +45,7 @@ public void testAlgorithmCustomizer() throws Exception { JsonWebKeySet jwkSet = new JsonWebKeySet("{\"keys\": [" + rsaJsonWebKey.toJson() + "]}"); OidcTenantConfig oidcConfig = new OidcTenantConfig(); - try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, null, null)) { + try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, null)) { try { provider.verifyJwtToken(newToken, false, false, null); fail("InvalidJwtException expected"); @@ -57,7 +61,7 @@ public JsonObject customizeHeaders(JsonObject headers) { return Json.createObjectBuilder(headers).add("alg", "RS256").build(); } - }, null)) { + }, null, null)) { TokenVerificationResult result = provider.verifyJwtToken(newToken, false, false, null); assertEquals("http://keycloak/realm", result.localVerificationResult.getString("iss")); } @@ -72,7 +76,7 @@ public void testTokenWithoutKidSingleRsaJwkWithoutKid() throws Exception { final String token = Jwt.issuer("http://keycloak/realm").sign(rsaJsonWebKey.getPrivateKey()); - try (OidcProvider provider = new OidcProvider(null, new OidcTenantConfig(), jwkSet, null, null)) { + try (OidcProvider provider = new OidcProvider(null, new OidcTenantConfig(), jwkSet, null)) { TokenVerificationResult result = provider.verifyJwtToken(token, false, false, null); assertEquals("http://keycloak/realm", result.localVerificationResult.getString("iss")); } @@ -87,7 +91,7 @@ public void testTokenWithoutKidMultipleRSAJwkWithoutKid() throws Exception { final String token = Jwt.issuer("http://keycloak/realm").sign(rsaJsonWebKey1.getPrivateKey()); - try (OidcProvider provider = new OidcProvider(null, new OidcTenantConfig(), jwkSet, null, null)) { + try (OidcProvider provider = new OidcProvider(null, new OidcTenantConfig(), jwkSet, null)) { try { provider.verifyJwtToken(token, false, false, null); fail("InvalidJwtException expected"); @@ -119,13 +123,13 @@ public void testSubject() throws Exception { final String tokenWithSub = Jwt.subject("subject").jws().keyId("k1").sign(rsaJsonWebKey.getPrivateKey()); - try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, null, null)) { + try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, null)) { TokenVerificationResult result = provider.verifyJwtToken(tokenWithSub, false, true, null); assertEquals("subject", result.localVerificationResult.getString(Claims.sub.name())); } final String tokenWithoutSub = Jwt.claims().jws().keyId("k1").sign(rsaJsonWebKey.getPrivateKey()); - try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, null, null)) { + try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, null)) { try { provider.verifyJwtToken(tokenWithoutSub, false, true, null); fail("InvalidJwtException expected"); @@ -146,13 +150,13 @@ public void testNonce() throws Exception { final String tokenWithNonce = Jwt.claim("nonce", "123456").jws().keyId("k1").sign(rsaJsonWebKey.getPrivateKey()); - try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, null, null)) { + try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, null)) { TokenVerificationResult result = provider.verifyJwtToken(tokenWithNonce, false, false, "123456"); assertEquals("123456", result.localVerificationResult.getString(Claims.nonce.name())); } final String tokenWithoutNonce = Jwt.claims().jws().keyId("k1").sign(rsaJsonWebKey.getPrivateKey()); - try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, null, null)) { + try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, null)) { try { provider.verifyJwtToken(tokenWithoutNonce, false, false, "123456"); fail("InvalidJwtException expected"); @@ -183,7 +187,7 @@ public void testAge() throws Exception { OidcTenantConfig oidcConfig = new OidcTenantConfig(); oidcConfig.token.issuedAtRequired = false; - try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, null, null)) { + try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, null)) { TokenVerificationResult result = provider.verifyJwtToken(token, false, false, null); assertNull(result.localVerificationResult.getString(Claims.iat.name())); } @@ -191,7 +195,7 @@ public void testAge() throws Exception { OidcTenantConfig oidcConfigRequireAge = new OidcTenantConfig(); oidcConfigRequireAge.token.issuedAtRequired = true; - try (OidcProvider provider = new OidcProvider(null, oidcConfigRequireAge, jwkSet, null, null)) { + try (OidcProvider provider = new OidcProvider(null, oidcConfigRequireAge, jwkSet, null)) { try { provider.verifyJwtToken(token, false, false, null); fail("InvalidJwtException expected"); @@ -200,4 +204,72 @@ public void testAge() throws Exception { } } } + + @Test + public void testJwtValidators() throws Exception { + RsaJsonWebKey rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048); + rsaJsonWebKey.setKeyId("k1"); + JsonWebKeySet jwkSet = new JsonWebKeySet("{\"keys\": [" + rsaJsonWebKey.toJson() + "]}"); + + OidcTenantConfig oidcConfig = new OidcTenantConfig(); + + String token = Jwt.claim("claim1", "claimValue1").claim("claim2", "claimValue2").jws().keyId("k1") + .sign(rsaJsonWebKey.getPrivateKey()); + + // no validators + try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, null, null, null)) { + TokenVerificationResult result = provider.verifyJwtToken(token, false, false, null); + assertEquals("claimValue1", result.localVerificationResult.getString("claim1")); + assertEquals("claimValue2", result.localVerificationResult.getString("claim2")); + } + + // one validator + Validator validator1 = new Validator() { + @Override + public String validate(JwtContext jwtContext) throws MalformedClaimException { + if (jwtContext.getJwtClaims().hasClaim("claim1")) { + return "Claim1 is not allowed!"; + } + return null; + } + }; + try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, null, null, List.of(validator1))) { + try { + provider.verifyJwtToken(token, false, false, null); + fail("InvalidJwtException expected"); + } catch (InvalidJwtException ex) { + assertTrue(ex.getMessage().contains("Claim1 is not allowed!")); + } + } + + // two validators + Validator validator2 = new Validator() { + @Override + public String validate(JwtContext jwtContext) throws MalformedClaimException { + if (jwtContext.getJwtClaims().hasClaim("claim2")) { + return "Claim2 is not allowed!"; + } + return null; + } + }; + // check the first validator is still run + try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, null, null, List.of(validator1, validator2))) { + try { + provider.verifyJwtToken(token, false, false, null); + fail("InvalidJwtException expected"); + } catch (InvalidJwtException ex) { + assertTrue(ex.getMessage().contains("Claim1 is not allowed!")); + } + } + // check the second validator is applied + token = Jwt.claim("claim2", "claimValue2").jws().keyId("k1").sign(rsaJsonWebKey.getPrivateKey()); + try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, null, null, List.of(validator1, validator2))) { + try { + provider.verifyJwtToken(token, false, false, null); + fail("InvalidJwtException expected"); + } catch (InvalidJwtException ex) { + assertTrue(ex.getMessage().contains("Claim2 is not allowed!")); + } + } + } } diff --git a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/GlobalJwtPreferredNameValidator.java b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/GlobalJwtPreferredNameValidator.java new file mode 100644 index 0000000000000..6dd00de4f79a5 --- /dev/null +++ b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/GlobalJwtPreferredNameValidator.java @@ -0,0 +1,24 @@ +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.Dependent; + +import org.jose4j.jwt.MalformedClaimException; +import org.jose4j.jwt.consumer.JwtContext; +import org.jose4j.jwt.consumer.Validator; + +import io.quarkus.arc.Unremovable; + +@Unremovable +@Dependent +public class GlobalJwtPreferredNameValidator implements Validator { + + @Override + public String validate(JwtContext jwtContext) throws MalformedClaimException { + if (jwtContext.getJwtClaims().hasClaim("preferred_username") + && jwtContext.getJwtClaims().isClaimValueString("preferred_username") + && jwtContext.getJwtClaims().getClaimValueAsString("preferred_username").contains("jdoe")) { + return "scope validation failed, the 'fail-validation' scope is not allowed"; + } + return null; + } +} diff --git a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/IssuerValidator.java b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/IssuerValidator.java new file mode 100644 index 0000000000000..4b687def82aa8 --- /dev/null +++ b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/IssuerValidator.java @@ -0,0 +1,28 @@ +package io.quarkus.it.keycloak; + +import static org.eclipse.microprofile.jwt.Claims.iss; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.jose4j.jwt.MalformedClaimException; +import org.jose4j.jwt.consumer.JwtContext; +import org.jose4j.jwt.consumer.Validator; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.TenantFeature; + +@Unremovable +@ApplicationScoped +@TenantFeature("tenant-public-key") +public class IssuerValidator implements Validator { + + @Override + public String validate(JwtContext jwtContext) throws MalformedClaimException { + if (jwtContext.getJwtClaims().hasClaim(iss.name()) + && "unacceptable-issuer".equals(jwtContext.getJwtClaims().getClaimValueAsString(iss.name()))) { + // issuer matched + return "The 'unacceptable-issuer' is not allowed"; + } + return null; + } +} diff --git a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantSpecificJwtPreferredNameValidator.java b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantSpecificJwtPreferredNameValidator.java new file mode 100644 index 0000000000000..c258e1299b7d3 --- /dev/null +++ b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantSpecificJwtPreferredNameValidator.java @@ -0,0 +1,36 @@ +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.jose4j.jwt.MalformedClaimException; +import org.jose4j.jwt.consumer.JwtContext; +import org.jose4j.jwt.consumer.Validator; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.TenantFeature; +import io.quarkus.oidc.runtime.OidcConfig; + +@Unremovable +@TenantFeature("tenant-requiredclaim") +@ApplicationScoped +public class TenantSpecificJwtPreferredNameValidator implements Validator { + + @Inject + OidcConfig oidcConfig; + + @Override + public String validate(JwtContext jwtContext) throws MalformedClaimException { + // verify that normal scoped validator is created when the runtime config is ready + if (!"quarkus-app-b".equals(oidcConfig.namedTenants.get("tenant-requiredclaim").token.requiredClaims.get("azp"))) { + throw new IllegalStateException("The 'tenant-requiredclaim' tenant required claim 'azp' is not 'quarkus-app-b'"); + } + + if (jwtContext.getJwtClaims().hasClaim("preferred_username") + && jwtContext.getJwtClaims().isClaimValueString("preferred_username") + && jwtContext.getJwtClaims().getClaimValueAsString("preferred_username").contains("admin")) { + return "scope validation failed, the 'fail-validation' scope is not allowed"; + } + return null; + } +} diff --git a/integration-tests/oidc-tenancy/src/main/resources/application.properties b/integration-tests/oidc-tenancy/src/main/resources/application.properties index 6f41166b32de7..c2253cdfb224d 100644 --- a/integration-tests/oidc-tenancy/src/main/resources/application.properties +++ b/integration-tests/oidc-tenancy/src/main/resources/application.properties @@ -121,6 +121,11 @@ quarkus.oidc.tenant-requiredclaim.auth-server-url=${keycloak.url}/realms/quarkus quarkus.oidc.tenant-requiredclaim.application-type=service quarkus.oidc.tenant-requiredclaim.token.required-claims.azp=quarkus-app-b +# same as 'tenant-requiredclaim' just with a different name +quarkus.oidc.tenant-requiredclaim-alternative.auth-server-url=${keycloak.url}/realms/quarkus-b +quarkus.oidc.tenant-requiredclaim-alternative.application-type=service +quarkus.oidc.tenant-requiredclaim-alternative.token.required-claims.azp=quarkus-app-b + quarkus.oidc.tenant-public-key.client-id=test quarkus.oidc.tenant-public-key.public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEqFyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwRTYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5eUF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYnsIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9xnQIDAQAB quarkus.oidc.tenant-public-key.tenant-paths=/api/tenant-paths/*/public-key,/api/tenant-paths/public-key/* 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 baa0238a4caec..e1fb18f39198d 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 @@ -654,6 +654,39 @@ public void testResolveTenantIdentifierWebAppDynamic() throws IOException { } } + @Test + public void testBothGlobalAndTenantSpecificJwtValidator() { + RestAssured.given().auth().oauth2(getAccessToken("alice", "b", "b")) + .when().get("/tenant/tenant-requiredclaim/api/user") + .then() + .statusCode(200); + RestAssured.given().auth().oauth2(getAccessToken("jdoe", "b", "b")) + .when().get("/tenant/tenant-requiredclaim/api/user") + .then() + .statusCode(401); + RestAssured.given().auth().oauth2(getAccessToken("admin", "b", "b")) + .when().get("/tenant/tenant-requiredclaim/api/user") + .then() + .statusCode(401); + } + + @Test + public void testGlobalJwtValidator() { + // tests that tenant-specific validator is not applied as the @TenantFeature value is not matched + RestAssured.given().auth().oauth2(getAccessToken("alice", "b", "b")) + .when().get("/tenant/tenant-requiredclaim-alternative/api/user") + .then() + .statusCode(200); + RestAssured.given().auth().oauth2(getAccessToken("jdoe", "b", "b")) + .when().get("/tenant/tenant-requiredclaim-alternative/api/user") + .then() + .statusCode(401); + RestAssured.given().auth().oauth2(getAccessToken("admin", "b", "b")) + .when().get("/tenant/tenant-requiredclaim-alternative/api/user") + .then() + .statusCode(200); + } + @Test public void testRequiredClaimPass() { //Client id should match the required azp claim diff --git a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/ServicePublicKeyTestCase.java b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/ServicePublicKeyTestCase.java index b358cf72f87ad..b202b72cf3f45 100644 --- a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/ServicePublicKeyTestCase.java +++ b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/ServicePublicKeyTestCase.java @@ -49,4 +49,18 @@ public void testExpiredToken() throws IOException, InterruptedException { .get("/service/tenant-public-key"); assertEquals(401, r.getStatusCode()); } + + @Test + public void testTokenIssuerVerification() { + String jwt = Jwt.claim("scope", "read:data").preferredUserName("alice").issuer("acceptable-issuer").sign(); + assertEquals("tenant-public-key:alice", RestAssured.given().auth() + .oauth2(jwt) + .get("/service/tenant-public-key").getBody().asString()); + jwt = Jwt.claim("scope", "read:data").preferredUserName("alice").issuer("unacceptable-issuer").sign(); + RestAssured.given().auth() + .oauth2(jwt) + .get("/service/tenant-public-key") + .then() + .statusCode(401); + } }