From e0af9888762e439058fd6d7b6f92dbdd7120aa07 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Mon, 1 Mar 2021 16:52:40 +0000 Subject: [PATCH] Update quarkus-oidc-token-propagation to better work with JWT tokens and update smalrye-jwt to 2.4.4 --- .github/native-tests.json | 2 +- bom/application/pom.xml | 14 +- docs/src/main/asciidoc/security-jwt.adoc | 1 + .../security-openid-connect-client.adoc | 75 ++++- .../oidc-token-propagation/deployment/pom.xml | 6 +- .../OidcTokenPropagationBuildStep.java | 27 +- .../oidc-token-propagation/runtime/pom.xml | 10 +- .../propagation/AccessTokenRequestFilter.java | 29 +- .../oidc/token/propagation/JsonWebToken.java | 13 + .../JsonWebTokenRequestFilter.java | 36 +++ .../runtime/AbstractTokenRequestFilter.java | 51 ++++ .../OidcTokenPropagationBuildTimeConfig.java | 16 ++ .../runtime/OidcTokenPropagationConfig.java | 30 +- .../runtime/OidcTokenCredentialProducer.java | 2 + .../jwt/deployment/SmallRyeJwtProcessor.java | 2 + .../jwt/runtime/auth/JWTAuthMechanism.java | 12 +- .../runtime/auth/JsonWebTokenCredential.java | 14 + .../auth/JsonWebTokenCredentialProducer.java | 32 +++ .../jwt/runtime/auth/MpJwtValidator.java | 1 + ...ava => AccessTokenPropagationService.java} | 2 +- .../quarkus/it/keycloak/FrontendResource.java | 19 +- .../keycloak/JwtTokenPropagationService.java | 17 ++ .../src/main/resources/application.properties | 3 +- ...=> OidcTokenPropagationInGraalITCase.java} | 2 +- .../it/keycloak/OidcTokenPropagationTest.java | 13 +- integration-tests/pom.xml | 1 + .../smallrye-jwt-token-propagation/pom.xml | 257 ++++++++++++++++++ .../AccessTokenPropagationService.java | 17 ++ .../quarkus/it/keycloak/FrontendResource.java | 56 ++++ .../JwtResignedProtectedResource.java | 46 ++++ .../keycloak/JwtTokenPropagationService.java | 17 ++ .../it/keycloak/ProtectedResource.java | 42 +++ .../src/main/resources/application.properties | 16 ++ .../src/main/resources/privateKey.pem | 28 ++ .../src/main/resources/publicKey.pem | 9 + .../KeycloakRealmResourceManager.java | 142 ++++++++++ .../OidcTokenPropagationInGraalITCase.java | 7 + .../it/keycloak/OidcTokenPropagationTest.java | 32 +++ 38 files changed, 1043 insertions(+), 56 deletions(-) create mode 100644 extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/JsonWebToken.java create mode 100644 extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/JsonWebTokenRequestFilter.java create mode 100644 extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/runtime/AbstractTokenRequestFilter.java create mode 100644 extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/runtime/OidcTokenPropagationBuildTimeConfig.java create mode 100644 extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JsonWebTokenCredential.java create mode 100644 extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JsonWebTokenCredentialProducer.java rename integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/{TokenPropagationService.java => AccessTokenPropagationService.java} (85%) create mode 100644 integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/JwtTokenPropagationService.java rename integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/{OidcClientInGraalITCase.java => OidcTokenPropagationInGraalITCase.java} (54%) create mode 100644 integration-tests/smallrye-jwt-token-propagation/pom.xml create mode 100644 integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/AccessTokenPropagationService.java create mode 100644 integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/FrontendResource.java create mode 100644 integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/JwtResignedProtectedResource.java create mode 100644 integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/JwtTokenPropagationService.java create mode 100644 integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java create mode 100644 integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties create mode 100644 integration-tests/smallrye-jwt-token-propagation/src/main/resources/privateKey.pem create mode 100644 integration-tests/smallrye-jwt-token-propagation/src/main/resources/publicKey.pem create mode 100644 integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java create mode 100644 integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationInGraalITCase.java create mode 100644 integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java diff --git a/.github/native-tests.json b/.github/native-tests.json index 02e36c98e7c01..542c5acc3e5a3 100644 --- a/.github/native-tests.json +++ b/.github/native-tests.json @@ -75,7 +75,7 @@ { "category": "Security2", "timeout": 70, - "test-modules": "oidc oidc-code-flow oidc-tenancy keycloak-authorization oidc-client oidc-token-propagation oidc-wiremock oidc-client-wiremock" + "test-modules": "oidc oidc-code-flow oidc-tenancy keycloak-authorization oidc-client oidc-token-propagation smallrye-jwt-token-propagation oidc-wiremock oidc-client-wiremock" }, { "category": "Security3", diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 2dee0ccb82323..5e60a29205b33 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -38,6 +38,7 @@ 1.3.3 1.0.1 1.4.1 + 1.1.1 1.5.0 1.11.1 2.2.6 @@ -46,7 +47,7 @@ 1.0.22 1.3.5 4.3.2 - 2.4.3 + 2.4.4 1.1.0 1.0.13 1.4.0 @@ -3579,6 +3580,17 @@ + + org.eclipse.microprofile.jwt + microprofile-jwt-auth-api + ${microprofile-jwt.version} + + + org.osgi + org.osgi.annotation.versioning + + + org.glassfish jakarta.el diff --git a/docs/src/main/asciidoc/security-jwt.adoc b/docs/src/main/asciidoc/security-jwt.adoc index 44740189f5028..54fa681eee19d 100644 --- a/docs/src/main/asciidoc/security-jwt.adoc +++ b/docs/src/main/asciidoc/security-jwt.adoc @@ -947,6 +947,7 @@ SmallRye JWT supports the following properties which can be used to customize th |smallrye.jwt.new-token.lifespan|300|Token lifespan in seconds which will be used to calculate an `exp` (expiry) claim value if this claim has not already been set. |smallrye.jwt.new-token.issuer|none|Token issuer which can be used to set an `iss` (issuer) claim value if this claim has not already been set. |smallrye.jwt.new-token.audience|none|Token audience which can be used to set an `aud` (audience) claim value if this claim has not already been set. +|smallrye.jwt.new-token.override-matching-claims|false| Set this property to `true` for `smallrye.jwt.new-token.issuer` and `smallrye.jwt.new-token.audience` values to override the already initialized `iss` (issuer) and `aud` (audience) claims. |=== == References diff --git a/docs/src/main/asciidoc/security-openid-connect-client.adoc b/docs/src/main/asciidoc/security-openid-connect-client.adoc index d144e0c8b0cb6..05d5e2e284c78 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client.adoc @@ -323,14 +323,20 @@ Using `private_key_jwt` or `private_key_jwt` authentication methods ensures that [[token-propagation]] == Token Propagation in MicroProfile RestClient client filter -`quarkus-oidc-token-propagation` extension provides `io.quarkus.oidc.token.propagation.AccessTokenRequestFilter` JAX-RS ClientRequestFilter which propagates the current link:security-openid-connect[Bearer] or link:security-openid-connect-web-authentication[Authorization Code Flow] access token as an HTTP `Authorization` `Bearer` scheme value. +`quarkus-oidc-token-propagation` extension provide `io.quarkus.oidc.token.propagation.AccessTokenRequestFilter` and `io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter` JAX-RS ClientRequestFilters which propagates the current link:security-openid-connect[Bearer] or link:security-openid-connect-web-authentication[Authorization Code Flow] access token as an HTTP `Authorization` `Bearer` scheme value. + +=== AccessTokenRequestFilter + +`AccessTokenRequestFilter` treats all tokens as Strings and as such it can work with both JWT and opaque tokens. + +When you need to propagate the current Authorization Code Flow access token then `AccessTokenRequestFilter` will be the best option as such tokens do not need to be exchanged or otherwise re-enhanced. Authorization Code Flow access tokens may be also be opaque/binary tokens. You can selectively register `AccessTokenRequestFilter` by using either `io.quarkus.oidc.token.propagation.AccessToken` or `org.eclipse.microprofile.rest.client.annotation.RegisterProvider`, for example: [source,java] ---- import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; -import io.quarkus.oidc.token.propagation.TokenCredential; +import io.quarkus.oidc.token.propagation.AccessToken; @RegisterRestClient @AccessToken @@ -359,9 +365,70 @@ public interface ProtectedResourceService { } ---- -Alternatively, `AccessTokenRequestFilter` can be registered automatically with all MP Rest or JAX-RS clients if `quarkus.oidc-token-propagation.register-filter=true` property is set. +Alternatively, `AccessTokenRequestFilter` can be registered automatically with all MP Rest or JAX-RS clients if `quarkus.oidc-token-propagation.register-filter` property is set to `true` and `quarkus.oidc-token-propagation.json-web-token` property is set to `false` (which is a default value). + +This filter will be additionally enhanced in the future to support exchanging the access tokens before propagating them. + +=== JsonWebTokenRequestFilter + +Using `JsonWebTokenRequestFilter` is recommended if you work with Bearer JWT tokens where these tokens can have their claims such as `issuer` and `audience` modified and the updated tokens secured (for example, re-signed) again. It expects an injected `org.eclipse.microprofile.jwt.JsonWebToken` and therefore will not work with the opaque tokens. + +Direct end to end Bearer token propagation should be avoided if possible. For example, `Client -> Service A -> Service B` where `Service B` receives a token sent by `Client` to `Service A`. In such cases `Service B` will not be able to distinguish if the token came from `Service A` or from `Client` directly. For `Service B` to verify the token came from `Service A` it should be able to assert a new issuer and audience claims. + +`JsonWebTokenRequestFilter` makes it easy for `Service A` implemementations to update the injected `org.eclipse.microprofile.jwt.JsonWebToken` with the new `issuer` and `audience` claim values and secure the updated token again with a new signature. The only difficult step is to ensure `Service A` has a signing key - it should be provisioned from a secure file system or from the remote secure storage such as Vault. + +You can selectively register `JsonWebTokenRequestFilter` by using either `io.quarkus.oidc.token.propagation.JsonWebToken` or `org.eclipse.microprofile.rest.client.annotation.RegisterProvider`, for example: + +[source,java] +---- +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import io.quarkus.oidc.token.propagation.JsonWebToken; + +@RegisterRestClient +@AccessToken +@Path("/") +public interface ProtectedResourceService { + + @GET + String getUserName(); +} +---- +or + +[source,java] +---- +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter; + +@RegisterRestClient +@RegisterProvider(JsonWebTokenTokenRequestFilter.class) +@Path("/") +public interface ProtectedResourceService { + + @GET + String getUserName(); +} +---- + +Alternatively, `JsonWebTokenRequestFilter` can be registered automatically with all MP Rest or JAX-RS clients if both `quarkus.oidc-token-propagation.register-filter` and ``quarkus.oidc-token-propagation.json-web-token` properties are set to `true`. + +If this filter has to update the inject token and secure it with a new signature again then you can configure it like this: + +[source,properties] +---- +quarkus.oidc-token-propagation.secure-json-web-token=true +smallrye.jwt.sign.key.location=/privateKey.pem +# Set a new issuer +smallrye.jwt.new-token.issuer=http://frontend-resource +# Set a new audience +smallrye.jwt.new-token.audience=http://downstream-resource +# Override the existing token issuer and audience claims if they are already set +smallrye.jwt.new-token.override-matching-claims=true +---- + -This filter will be enhanced in the future to support re-signing and/or exchanging the access tokens before propagating them. +This filter will be additionally enhanced in the future to support exchanging the access tokens before propagating them. == References diff --git a/extensions/oidc-token-propagation/deployment/pom.xml b/extensions/oidc-token-propagation/deployment/pom.xml index 13af26155cd94..fea83659f5972 100644 --- a/extensions/oidc-token-propagation/deployment/pom.xml +++ b/extensions/oidc-token-propagation/deployment/pom.xml @@ -20,12 +20,16 @@ io.quarkus - quarkus-oidc-deployment + quarkus-security-deployment io.quarkus quarkus-rest-client-deployment + + io.quarkus + quarkus-smallrye-jwt-build-deployment + diff --git a/extensions/oidc-token-propagation/deployment/src/main/java/io/quarkus/oidc/token/propagation/deployment/OidcTokenPropagationBuildStep.java b/extensions/oidc-token-propagation/deployment/src/main/java/io/quarkus/oidc/token/propagation/deployment/OidcTokenPropagationBuildStep.java index 9f190dc9b820b..d3511f4f1db79 100644 --- a/extensions/oidc-token-propagation/deployment/src/main/java/io/quarkus/oidc/token/propagation/deployment/OidcTokenPropagationBuildStep.java +++ b/extensions/oidc-token-propagation/deployment/src/main/java/io/quarkus/oidc/token/propagation/deployment/OidcTokenPropagationBuildStep.java @@ -1,5 +1,7 @@ package io.quarkus.oidc.token.propagation.deployment; +import java.util.function.BooleanSupplier; + import org.jboss.jandex.DotName; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; @@ -9,16 +11,19 @@ import io.quarkus.deployment.builditem.EnableAllSecurityServicesBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; -import io.quarkus.oidc.deployment.OidcBuildStep.IsEnabled; import io.quarkus.oidc.token.propagation.AccessToken; import io.quarkus.oidc.token.propagation.AccessTokenRequestFilter; +import io.quarkus.oidc.token.propagation.JsonWebToken; +import io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter; +import io.quarkus.oidc.token.propagation.runtime.OidcTokenPropagationBuildTimeConfig; import io.quarkus.oidc.token.propagation.runtime.OidcTokenPropagationConfig; import io.quarkus.restclient.deployment.RestClientAnnotationProviderBuildItem; import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; public class OidcTokenPropagationBuildStep { - private static final DotName TOKEN_CREDENTIAL = DotName.createSimple(AccessToken.class.getName()); + private static final DotName ACCESS_TOKEN_CREDENTIAL = DotName.createSimple(AccessToken.class.getName()); + private static final DotName JWT_ACCESS_TOKEN_CREDENTIAL = DotName.createSimple(JsonWebToken.class.getName()); OidcTokenPropagationConfig config; @@ -38,12 +43,26 @@ void registerProvider(BuildProducer additionalBeans, BuildProducer jaxrsProviders, BuildProducer restAnnotationProvider) { additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(AccessTokenRequestFilter.class)); + additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(JsonWebTokenRequestFilter.class)); reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, AccessTokenRequestFilter.class)); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, JsonWebTokenRequestFilter.class)); + if (config.registerFilter) { - jaxrsProviders.produce(new ResteasyJaxrsProviderBuildItem(AccessTokenRequestFilter.class.getName())); + Class filterClass = config.jsonWebToken ? JsonWebTokenRequestFilter.class : AccessTokenRequestFilter.class; + jaxrsProviders.produce(new ResteasyJaxrsProviderBuildItem(filterClass.getName())); } else { - restAnnotationProvider.produce(new RestClientAnnotationProviderBuildItem(TOKEN_CREDENTIAL, + restAnnotationProvider.produce(new RestClientAnnotationProviderBuildItem(ACCESS_TOKEN_CREDENTIAL, AccessTokenRequestFilter.class)); + restAnnotationProvider.produce(new RestClientAnnotationProviderBuildItem(JWT_ACCESS_TOKEN_CREDENTIAL, + JsonWebTokenRequestFilter.class)); + } + } + + public static class IsEnabled implements BooleanSupplier { + OidcTokenPropagationBuildTimeConfig config; + + public boolean getAsBoolean() { + return config.enabled; } } } diff --git a/extensions/oidc-token-propagation/runtime/pom.xml b/extensions/oidc-token-propagation/runtime/pom.xml index 338203b742d9d..a3c575cf2e79b 100644 --- a/extensions/oidc-token-propagation/runtime/pom.xml +++ b/extensions/oidc-token-propagation/runtime/pom.xml @@ -16,12 +16,20 @@ io.quarkus - quarkus-oidc + quarkus-security + + + org.eclipse.microprofile.jwt + microprofile-jwt-auth-api io.quarkus quarkus-rest-client + + io.quarkus + quarkus-smallrye-jwt-build + diff --git a/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/AccessTokenRequestFilter.java b/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/AccessTokenRequestFilter.java index 6c48d7683575c..321573ed3c80c 100644 --- a/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/AccessTokenRequestFilter.java +++ b/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/AccessTokenRequestFilter.java @@ -2,37 +2,22 @@ import java.io.IOException; -import javax.annotation.Priority; +import javax.enterprise.inject.Instance; import javax.inject.Inject; -import javax.inject.Singleton; -import javax.ws.rs.Priorities; import javax.ws.rs.client.ClientRequestContext; -import javax.ws.rs.client.ClientRequestFilter; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.Provider; -import org.jboss.logging.Logger; +import io.quarkus.oidc.token.propagation.runtime.AbstractTokenRequestFilter; +import io.quarkus.security.credential.TokenCredential; -import io.quarkus.oidc.AccessTokenCredential; - -@Provider -@Singleton -@Priority(Priorities.AUTHENTICATION) -public class AccessTokenRequestFilter implements ClientRequestFilter { - private static final Logger LOG = Logger.getLogger(AccessTokenRequestFilter.class); - private static final String BEARER_SCHEME_WITH_SPACE = "Bearer "; +public class AccessTokenRequestFilter extends AbstractTokenRequestFilter { @Inject - AccessTokenCredential tokenCredential; + Instance accessToken; @Override public void filter(ClientRequestContext requestContext) throws IOException { - try { - requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, BEARER_SCHEME_WITH_SPACE + tokenCredential.getToken()); - } catch (Exception ex) { - LOG.debugf("Access token is not available, aborting the request with HTTP 401 error: %s", ex.getMessage()); - requestContext.abortWith(Response.status(401).build()); + if (verifyTokenInstance(requestContext, accessToken)) { + propagateToken(requestContext, accessToken.get().getToken()); } } } diff --git a/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/JsonWebToken.java b/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/JsonWebToken.java new file mode 100644 index 0000000000000..e7b8601602bd6 --- /dev/null +++ b/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/JsonWebToken.java @@ -0,0 +1,13 @@ +package io.quarkus.oidc.token.propagation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface JsonWebToken { +} diff --git a/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/JsonWebTokenRequestFilter.java b/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/JsonWebTokenRequestFilter.java new file mode 100644 index 0000000000000..4151e696ac946 --- /dev/null +++ b/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/JsonWebTokenRequestFilter.java @@ -0,0 +1,36 @@ +package io.quarkus.oidc.token.propagation; + +import java.io.IOException; + +import javax.enterprise.inject.Instance; +import javax.inject.Inject; +import javax.ws.rs.client.ClientRequestContext; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import io.quarkus.oidc.token.propagation.runtime.AbstractTokenRequestFilter; +import io.smallrye.jwt.build.Jwt; + +public class JsonWebTokenRequestFilter extends AbstractTokenRequestFilter { + @Inject + Instance jwtAccessToken; + + @Inject + @ConfigProperty(name = "quarkus.oidc-token-propagation.secure-json-web-token") + boolean resignToken; + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + if (verifyTokenInstance(requestContext, jwtAccessToken)) { + propagateToken(requestContext, getToken()); + } + } + + private String getToken() { + if (resignToken) { + return Jwt.claims(jwtAccessToken.get()).sign(); + } else { + return jwtAccessToken.get().getRawToken(); + } + } +} diff --git a/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/runtime/AbstractTokenRequestFilter.java b/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/runtime/AbstractTokenRequestFilter.java new file mode 100644 index 0000000000000..262ca3089b2d5 --- /dev/null +++ b/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/runtime/AbstractTokenRequestFilter.java @@ -0,0 +1,51 @@ +package io.quarkus.oidc.token.propagation.runtime; + +import java.io.IOException; + +import javax.annotation.Priority; +import javax.enterprise.inject.Instance; +import javax.inject.Singleton; +import javax.ws.rs.Priorities; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +import org.jboss.logging.Logger; + +@Provider +@Singleton +@Priority(Priorities.AUTHENTICATION) +public abstract class AbstractTokenRequestFilter implements ClientRequestFilter { + private static final Logger LOG = Logger.getLogger(AbstractTokenRequestFilter.class); + private static final String BEARER_SCHEME_WITH_SPACE = "Bearer "; + + public void propagateToken(ClientRequestContext requestContext, String token) throws IOException { + if (token != null) { + requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, BEARER_SCHEME_WITH_SPACE + token); + } else { + LOG.debugf("Injected access token is null, aborting the request with HTTP 401 error"); + abortRequest(requestContext); + } + } + + protected boolean verifyTokenInstance(ClientRequestContext requestContext, Instance instance) throws IOException { + if (!instance.isResolvable()) { + LOG.debugf("Access token is not injected, aborting the request with HTTP 401 error"); + abortRequest(requestContext); + return false; + } + if (instance.isAmbiguous()) { + LOG.debugf("More than one access token instance is available, aborting the request with HTTP 401 error"); + abortRequest(requestContext); + return false; + } + + return true; + } + + protected void abortRequest(ClientRequestContext requestContext) { + requestContext.abortWith(Response.status(401).build()); + } +} diff --git a/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/runtime/OidcTokenPropagationBuildTimeConfig.java b/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/runtime/OidcTokenPropagationBuildTimeConfig.java new file mode 100644 index 0000000000000..84c1feda61bae --- /dev/null +++ b/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/runtime/OidcTokenPropagationBuildTimeConfig.java @@ -0,0 +1,16 @@ +package io.quarkus.oidc.token.propagation.runtime; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * Build time configuration for OIDC Token Propagation. + */ +@ConfigRoot +public class OidcTokenPropagationBuildTimeConfig { + /** + * If the OIDC Token Propagation is enabled. + */ + @ConfigItem(defaultValue = "true") + public boolean enabled; +} diff --git a/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/runtime/OidcTokenPropagationConfig.java b/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/runtime/OidcTokenPropagationConfig.java index f4824e8207ae6..30db061614909 100644 --- a/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/runtime/OidcTokenPropagationConfig.java +++ b/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/runtime/OidcTokenPropagationConfig.java @@ -7,9 +7,35 @@ @ConfigRoot(name = "oidc-token-propagation", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) public class OidcTokenPropagationConfig { /** - * Enable TokenCredentialFilter for all the injected MP RestClient implementations. - * If this property is disabled then TokenCredentialRequestFilter has to be registered as an MP RestClient provider. + * Enable either AccessTokenRequestFilter or JsonWebTokenRequestFilter for all the injected MP RestClient implementations. + * + * AccessTokenRequestFilter can propagate both opaque (binary) and JsonWebToken tokens but it can not modify + * and secure the updated JsonWebToken tokens. + * JsonWebTokenRequestFilter can only propagate JsonWebToken tokens but it can also modify and secure them again. + * Enable the 'jsonWebToken' property to have JsonWebTokenRequestFilter registered. + * + * Alternatively, instead of using this property for registering these filters with all the injected MP RestClient + * implementations, both filters can be registered as MP RestClient providers with the specific MP RestClient + * implementations. */ @ConfigItem(defaultValue = "false") public boolean registerFilter; + + /** + * Enable JsonWebTokenRequestFilter instead of AccessTokenRequestFilter for all the injected MP RestClient implementations. + * This filter can propagate as well as modify and secure the updated JsonWebToken tokens. + * + * Note this property is ignored unless the 'registerFilter' property is enabled. + */ + @ConfigItem(defaultValue = "false") + public boolean jsonWebToken; + + /** + * Secure the injected and possibly modified JsonWebToken. + * For example, a JsonWebToken produced and signed by OpenId Connect provider can be re-signed using a new private key. + * + * Note this property is injected into JsonWebTokenRequestFilter. + */ + @ConfigItem(defaultValue = "false") + public boolean secureJsonWebToken; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTokenCredentialProducer.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTokenCredentialProducer.java index 55d946ac6fff2..d9d830ac1374d 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTokenCredentialProducer.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTokenCredentialProducer.java @@ -6,6 +6,7 @@ import org.jboss.logging.Logger; +import io.quarkus.arc.AlternativePriority; import io.quarkus.oidc.AccessTokenCredential; import io.quarkus.oidc.IdTokenCredential; import io.quarkus.oidc.OIDCException; @@ -37,6 +38,7 @@ IdTokenCredential currentIdToken() { @Produces @RequestScoped + @AlternativePriority(1) AccessTokenCredential currentAccessToken() { AccessTokenCredential cred = identity.getCredential(AccessTokenCredential.class); if (cred == null || cred.getToken() == null) { diff --git a/extensions/smallrye-jwt/deployment/src/main/java/io/quarkus/smallrye/jwt/deployment/SmallRyeJwtProcessor.java b/extensions/smallrye-jwt/deployment/src/main/java/io/quarkus/smallrye/jwt/deployment/SmallRyeJwtProcessor.java index f767caa75cecc..7a3fc6022f35e 100644 --- a/extensions/smallrye-jwt/deployment/src/main/java/io/quarkus/smallrye/jwt/deployment/SmallRyeJwtProcessor.java +++ b/extensions/smallrye-jwt/deployment/src/main/java/io/quarkus/smallrye/jwt/deployment/SmallRyeJwtProcessor.java @@ -32,6 +32,7 @@ import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.security.deployment.JCAProviderBuildItem; import io.quarkus.smallrye.jwt.runtime.auth.JWTAuthMechanism; +import io.quarkus.smallrye.jwt.runtime.auth.JsonWebTokenCredentialProducer; import io.quarkus.smallrye.jwt.runtime.auth.JwtPrincipalProducer; import io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator; import io.quarkus.smallrye.jwt.runtime.auth.RawOptionalClaimCreator; @@ -73,6 +74,7 @@ void registerAdditionalBeans(BuildProducer additionalBe if (config.enabled) { AdditionalBeanBuildItem.Builder unremovable = AdditionalBeanBuildItem.builder().setUnremovable(); unremovable.addBeanClass(MpJwtValidator.class); + unremovable.addBeanClass(JsonWebTokenCredentialProducer.class); unremovable.addBeanClass(JWTAuthMechanism.class); unremovable.addBeanClass(ClaimValueProducer.class); additionalBeans.produce(unremovable.build()); diff --git a/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JWTAuthMechanism.java b/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JWTAuthMechanism.java index 94f42562564d2..c3ebafd108f65 100644 --- a/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JWTAuthMechanism.java +++ b/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JWTAuthMechanism.java @@ -7,15 +7,11 @@ import java.util.Set; import javax.enterprise.context.ApplicationScoped; -import javax.enterprise.inject.spi.CDI; import javax.inject.Inject; -import org.eclipse.microprofile.jwt.JsonWebToken; - import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.cookie.ServerCookieDecoder; -import io.quarkus.security.credential.TokenCredential; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.AuthenticationRequest; @@ -24,7 +20,6 @@ import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport; import io.smallrye.jwt.auth.AbstractBearerTokenExtractor; -import io.smallrye.jwt.auth.cdi.PrincipalProducer; import io.smallrye.jwt.auth.principal.JWTAuthContextInfo; import io.smallrye.mutiny.Uni; import io.vertx.ext.web.Cookie; @@ -42,18 +37,13 @@ public class JWTAuthMechanism implements HttpAuthenticationMechanism { @Inject private JWTAuthContextInfo authContextInfo; - private void preparePrincipalProducer(JsonWebToken jwtPrincipal) { - PrincipalProducer principalProducer = CDI.current().select(PrincipalProducer.class).get(); - principalProducer.setJsonWebToken(jwtPrincipal); - } - @Override public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { String jwtToken = new VertxBearerTokenExtractor(authContextInfo, context).getBearerToken(); if (jwtToken != null) { return identityProviderManager - .authenticate(new TokenAuthenticationRequest(new TokenCredential(jwtToken, "bearer"))); + .authenticate(new TokenAuthenticationRequest(new JsonWebTokenCredential(jwtToken))); } return Uni.createFrom().optional(Optional.empty()); } diff --git a/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JsonWebTokenCredential.java b/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JsonWebTokenCredential.java new file mode 100644 index 0000000000000..9a0e93e335870 --- /dev/null +++ b/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JsonWebTokenCredential.java @@ -0,0 +1,14 @@ +package io.quarkus.smallrye.jwt.runtime.auth; + +import io.quarkus.security.credential.TokenCredential; + +public class JsonWebTokenCredential extends TokenCredential { + + public JsonWebTokenCredential() { + this(null); + } + + public JsonWebTokenCredential(String token) { + super(token, "bearer"); + } +} diff --git a/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JsonWebTokenCredentialProducer.java b/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JsonWebTokenCredentialProducer.java new file mode 100644 index 0000000000000..d493f982e4047 --- /dev/null +++ b/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JsonWebTokenCredentialProducer.java @@ -0,0 +1,32 @@ +package io.quarkus.smallrye.jwt.runtime.auth; + +import javax.enterprise.context.RequestScoped; +import javax.enterprise.inject.Produces; +import javax.inject.Inject; + +import org.jboss.logging.Logger; + +import io.quarkus.security.identity.SecurityIdentity; + +@RequestScoped +public class JsonWebTokenCredentialProducer { + private static final Logger LOG = Logger.getLogger(JsonWebTokenCredentialProducer.class); + @Inject + SecurityIdentity identity; + + /** + * The producer method for the current id token + * + * @return the id token + */ + @Produces + @RequestScoped + JsonWebTokenCredential currentToken() { + JsonWebTokenCredential cred = identity.getCredential(JsonWebTokenCredential.class); + if (cred == null || cred.getToken() == null) { + LOG.trace("JsonWebToken is null"); + cred = new JsonWebTokenCredential(); + } + return cred; + } +} diff --git a/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/MpJwtValidator.java b/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/MpJwtValidator.java index a1c0924e599c6..47c31cb63b114 100644 --- a/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/MpJwtValidator.java +++ b/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/MpJwtValidator.java @@ -52,6 +52,7 @@ public void accept(UniEmitter uniEmitter) { try { JsonWebToken jwtPrincipal = parser.parse(request.getToken().getToken()); uniEmitter.complete(QuarkusSecurityIdentity.builder().setPrincipal(jwtPrincipal) + .addCredential(request.getToken()) .addRoles(jwtPrincipal.getGroups()) .addAttribute(SecurityIdentity.USER_ATTRIBUTE, jwtPrincipal).build()); diff --git a/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/TokenPropagationService.java b/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/AccessTokenPropagationService.java similarity index 85% rename from integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/TokenPropagationService.java rename to integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/AccessTokenPropagationService.java index ac1c20bea4666..a85798bfccdbd 100644 --- a/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/TokenPropagationService.java +++ b/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/AccessTokenPropagationService.java @@ -10,7 +10,7 @@ @RegisterRestClient @AccessToken @Path("/") -public interface TokenPropagationService { +public interface AccessTokenPropagationService { @GET String getUserName(); diff --git a/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/FrontendResource.java b/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/FrontendResource.java index 742a206b563f6..24b1004ec0947 100644 --- a/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/FrontendResource.java +++ b/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/FrontendResource.java @@ -11,17 +11,28 @@ public class FrontendResource { @Inject @RestClient - TokenPropagationService tokenPropagationService; + JwtTokenPropagationService jwtTokenPropagationService; + + @Inject + @RestClient + AccessTokenPropagationService accessTokenPropagationService; @Inject @RestClient ServiceAccountService serviceAccountService; @GET - @Path("token-propagation") + @Path("jwt-token-propagation") + @RolesAllowed("user") + public String userNameJwtTokenPropagation() { + return jwtTokenPropagationService.getUserName(); + } + + @GET + @Path("access-token-propagation") @RolesAllowed("user") - public String userNameTokenPropagation() { - return tokenPropagationService.getUserName(); + public String userNameAccessTokenPropagation() { + return accessTokenPropagationService.getUserName(); } @GET diff --git a/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/JwtTokenPropagationService.java b/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/JwtTokenPropagationService.java new file mode 100644 index 0000000000000..c5c8b87eb9545 --- /dev/null +++ b/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/JwtTokenPropagationService.java @@ -0,0 +1,17 @@ +package io.quarkus.it.keycloak; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.quarkus.oidc.token.propagation.JsonWebToken; + +@RegisterRestClient +@JsonWebToken +@Path("/") +public interface JwtTokenPropagationService { + + @GET + String getUserName(); +} diff --git a/integration-tests/oidc-token-propagation/src/main/resources/application.properties b/integration-tests/oidc-token-propagation/src/main/resources/application.properties index db1575f3f07a7..8d813f9172cbe 100644 --- a/integration-tests/oidc-token-propagation/src/main/resources/application.properties +++ b/integration-tests/oidc-token-propagation/src/main/resources/application.properties @@ -9,6 +9,7 @@ quarkus.oidc-client.grant.type=password quarkus.oidc-client.grant-options.password.username=bob quarkus.oidc-client.grant-options.password.password=bob -io.quarkus.it.keycloak.TokenPropagationService/mp-rest/uri=http://localhost:8081/protected +io.quarkus.it.keycloak.JwtTokenPropagationService/mp-rest/uri=http://localhost:8081/protected +io.quarkus.it.keycloak.AccessTokenPropagationService/mp-rest/uri=http://localhost:8081/protected io.quarkus.it.keycloak.ServiceAccountService/mp-rest/uri=http://localhost:8081/protected diff --git a/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcClientInGraalITCase.java b/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationInGraalITCase.java similarity index 54% rename from integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcClientInGraalITCase.java rename to integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationInGraalITCase.java index 7a876b2d312e1..01ee8ab1acbd6 100644 --- a/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcClientInGraalITCase.java +++ b/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationInGraalITCase.java @@ -3,5 +3,5 @@ import io.quarkus.test.junit.NativeImageTest; @NativeImageTest -public class OidcClientInGraalITCase extends OidcTokenPropagationTest { +public class OidcTokenPropagationInGraalITCase extends OidcTokenPropagationTest { } diff --git a/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java b/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java index 9c97d867425c1..836310c666edb 100644 --- a/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java +++ b/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java @@ -13,9 +13,18 @@ public class OidcTokenPropagationTest { @Test - public void testGetUserNameWithTokenPropagation() { + public void testGetUserNameWithJwtTokenPropagation() { RestAssured.given().auth().oauth2(KeycloakRealmResourceManager.getAccessToken("alice")) - .when().get("/frontend/token-propagation") + .when().get("/frontend/jwt-token-propagation") + .then() + .statusCode(200) + .body(equalTo("alice")); + } + + @Test + public void testGetUserNameWithAccessTokenPropagation() { + RestAssured.given().auth().oauth2(KeycloakRealmResourceManager.getAccessToken("alice")) + .when().get("/frontend/access-token-propagation") .then() .statusCode(200) .body(equalTo("alice")); diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 349dcc97d394d..d4fb98cedb80d 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -83,6 +83,7 @@ oidc-client oidc-client-wiremock oidc-token-propagation + smallrye-jwt-token-propagation oidc-code-flow oidc-tenancy oidc-wiremock diff --git a/integration-tests/smallrye-jwt-token-propagation/pom.xml b/integration-tests/smallrye-jwt-token-propagation/pom.xml new file mode 100644 index 0000000000000..c0151077f9f99 --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/pom.xml @@ -0,0 +1,257 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-integration-test-smallrye-jwt-token-propagation + Quarkus - Integration Tests - Smallrye JWT Token Propagation + Module that contains Smallrye JWT Token Propagation tests + + + http://localhost:8180/auth + + + + + org.keycloak + keycloak-adapter-core + + + org.keycloak + keycloak-core + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-resteasy-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-oidc-token-propagation + + + io.quarkus + quarkus-oidc-token-propagation-deployment + ${project.version} + pom + test + + + * + * + + + + + + io.quarkus + quarkus-smallrye-jwt + + + io.quarkus + quarkus-smallrye-jwt-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + src/main/resources + true + + + + + maven-surefire-plugin + + true + + + + maven-failsafe-plugin + + true + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + test-keycloak + + + test-containers + + + + + + maven-surefire-plugin + + false + + ${keycloak.url} + + + + + maven-failsafe-plugin + + false + + ${keycloak.url} + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + docker-keycloak + + + start-containers + + + + http://localhost:8180/auth + + + + + io.fabric8 + docker-maven-plugin + + + + ${keycloak.docker.image} + quarkus-test-keycloak + + + 8180:8080 + + + admin + admin + + + Keycloak: + default + cyan + + + + + http://localhost:8180 + + + + + + + true + + + + docker-start + compile + + stop + start + + + + docker-stop + post-integration-test + + stop + + + + + + org.codehaus.mojo + exec-maven-plugin + + + docker-prune + generate-resources + + exec + + + ${basedir}/../../.github/docker-prune.sh + + + + + + + + + + + diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/AccessTokenPropagationService.java b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/AccessTokenPropagationService.java new file mode 100644 index 0000000000000..a85798bfccdbd --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/AccessTokenPropagationService.java @@ -0,0 +1,17 @@ +package io.quarkus.it.keycloak; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.quarkus.oidc.token.propagation.AccessToken; + +@RegisterRestClient +@AccessToken +@Path("/") +public interface AccessTokenPropagationService { + + @GET + String getUserName(); +} diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/FrontendResource.java b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/FrontendResource.java new file mode 100644 index 0000000000000..e766dc26103dd --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/FrontendResource.java @@ -0,0 +1,56 @@ +package io.quarkus.it.keycloak; + +import javax.annotation.security.RolesAllowed; +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +@Path("/frontend") +public class FrontendResource { + @Inject + JsonWebToken jwt; + + @Inject + @ConfigProperty(name = "mp.jwt.verify.issuer") + String configuredIssuer; + + @Inject + @RestClient + JwtTokenPropagationService jwtTokenPropagationService; + + @Inject + @RestClient + AccessTokenPropagationService accessTokenPropagationService; + + @GET + @Path("jwt-token-propagation") + @RolesAllowed("user") + public String userNameJwtTokenPropagation() { + checkIssuerAndAudience(); + return jwtTokenPropagationService.getUserName(); + } + + @GET + @Path("access-token-propagation") + @RolesAllowed("user") + public String userNameAccessTokenPropagation() { + checkIssuerAndAudience(); + return accessTokenPropagationService.getUserName(); + } + + private void checkIssuerAndAudience() { + // it has already been verified by smallrye-jwt + if (!configuredIssuer.equals(jwt.getIssuer())) { + throw new NotAuthorizedException(401); + } + if (jwt.getAudience() != null) { + // Keycloak does not set the audience + throw new NotAuthorizedException(401); + } + } +} diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/JwtResignedProtectedResource.java b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/JwtResignedProtectedResource.java new file mode 100644 index 0000000000000..a897f7451ecd2 --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/JwtResignedProtectedResource.java @@ -0,0 +1,46 @@ +package io.quarkus.it.keycloak; + +import java.security.PublicKey; +import java.util.Set; + +import javax.annotation.PostConstruct; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.smallrye.jwt.auth.principal.DefaultJWTParser; +import io.smallrye.jwt.auth.principal.JWTAuthContextInfo; +import io.smallrye.jwt.auth.principal.JWTParser; +import io.smallrye.jwt.util.KeyUtils; + +@Path("/jwt-resigned-protected") +public class JwtResignedProtectedResource { + + JWTParser parser; + + @PostConstruct + public void loadVerificationKey() throws Exception { + PublicKey verificationKey = KeyUtils.readPublicKey("/publicKey.pem"); + parser = new DefaultJWTParser(new JWTAuthContextInfo(verificationKey, "http://frontend-resource")); + } + + @GET + public String principalName(@HeaderParam("Authorization") String authorization) throws Exception { + JsonWebToken jwt = parser.parse(authorization.split(" ")[1]); + checkIssuerAndAudience(jwt); + return jwt.getName(); + } + + private void checkIssuerAndAudience(JsonWebToken jwt) { + if (!"http://frontend-resource".equals(jwt.getIssuer())) { + throw new NotAuthorizedException(401); + } + Set aud = jwt.getAudience(); + if (aud.size() != 1 || !aud.contains("http://jwt-resigned-protected-resource")) { + throw new NotAuthorizedException(401); + } + } +} diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/JwtTokenPropagationService.java b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/JwtTokenPropagationService.java new file mode 100644 index 0000000000000..c5c8b87eb9545 --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/JwtTokenPropagationService.java @@ -0,0 +1,17 @@ +package io.quarkus.it.keycloak; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.quarkus.oidc.token.propagation.JsonWebToken; + +@RegisterRestClient +@JsonWebToken +@Path("/") +public interface JwtTokenPropagationService { + + @GET + String getUserName(); +} diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java new file mode 100644 index 0000000000000..a16ffa8cf26fc --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java @@ -0,0 +1,42 @@ +package io.quarkus.it.keycloak; + +import javax.annotation.security.RolesAllowed; +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.security.Authenticated; + +@Path("/protected") +@Authenticated +public class ProtectedResource { + + @Inject + JsonWebToken jwt; + + @Inject + @ConfigProperty(name = "mp.jwt.verify.issuer") + String configuredIssuer; + + @GET + @RolesAllowed("user") + public String principalName() { + checkIssuerAndAudience(); + return jwt.getName(); + } + + private void checkIssuerAndAudience() { + // it has already been verified by smallrye-jwt + if (!configuredIssuer.equals(jwt.getIssuer())) { + throw new NotAuthorizedException(401); + } + if (jwt.getAudience() != null) { + // Keycloak does not set the audience + throw new NotAuthorizedException(401); + } + } +} diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties b/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties new file mode 100644 index 0000000000000..e4ff9949fbbaf --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties @@ -0,0 +1,16 @@ +mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs +mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus +smallrye.jwt.path.groups=realm_access/roles + +io.quarkus.it.keycloak.JwtTokenPropagationService/mp-rest/uri=http://localhost:8081/jwt-resigned-protected +io.quarkus.it.keycloak.AccessTokenPropagationService/mp-rest/uri=http://localhost:8081/protected + +quarkus.oidc-token-propagation.secure-json-web-token=true +smallrye.jwt.sign.key.location=/privateKey.pem +smallrye.jwt.new-token.issuer=http://frontend-resource +smallrye.jwt.new-token.audience=http://jwt-resigned-protected-resource +smallrye.jwt.new-token.override-matching-claims=true + +quarkus.http.auth.proactive=false + +quarkus.native.additional-build-args=-H:IncludeResources=.*\\.pem \ No newline at end of file diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/resources/privateKey.pem b/integration-tests/smallrye-jwt-token-propagation/src/main/resources/privateKey.pem new file mode 100644 index 0000000000000..27543a434a1eb --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/resources/privateKey.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWK8UjyoHgPTLa +PLQJ8SoXLLjpHSjtLxMqmzHnFscqhTVVaDpCRCb6e3Ii/WniQTWw8RA7vf4djz4H +OzvlfBFNgvUGZHXDwnmGaNVaNzpHYFMEYBhE8VGGiveSkzqeLZI+Y02G6sQAfDtN +qqzM/l5QX8X34oQFaTBW1r49nftvCpITiwJvWyhkWtXP9RP8sXi1im5Vi3dhupOh +nelk5n0BfajUYIbfHA6ORzjHRbt7NtBl0L2J+0/FUdHyKs6KMlFGNw8O0Dq88qnM +uXoLJiewhg9332W3DFMeOveel+//cvDnRsCRtPgd4sXFPHh+UShkso7+DRsChXa6 +oGGQD3GdAgMBAAECggEAAjfTSZwMHwvIXIDZB+yP+pemg4ryt84iMlbofclQV8hv +6TsI4UGwcbKxFOM5VSYxbNOisb80qasb929gixsyBjsQ8284bhPJR7r0q8h1C+jY +URA6S4pk8d/LmFakXwG9Tz6YPo3pJziuh48lzkFTk0xW2Dp4SLwtAptZY/+ZXyJ6 +96QXDrZKSSM99Jh9s7a0ST66WoxSS0UC51ak+Keb0KJ1jz4bIJ2C3r4rYlSu4hHB +Y73GfkWORtQuyUDa9yDOem0/z0nr6pp+pBSXPLHADsqvZiIhxD/O0Xk5I6/zVHB3 +zuoQqLERk0WvA8FXz2o8AYwcQRY2g30eX9kU4uDQAQKBgQDmf7KGImUGitsEPepF +KH5yLWYWqghHx6wfV+fdbBxoqn9WlwcQ7JbynIiVx8MX8/1lLCCe8v41ypu/eLtP +iY1ev2IKdrUStvYRSsFigRkuPHUo1ajsGHQd+ucTDf58mn7kRLW1JGMeGxo/t32B +m96Af6AiPWPEJuVfgGV0iwg+HQKBgQCmyPzL9M2rhYZn1AozRUguvlpmJHU2DpqS +34Q+7x2Ghf7MgBUhqE0t3FAOxEC7IYBwHmeYOvFR8ZkVRKNF4gbnF9RtLdz0DMEG +5qsMnvJUSQbNB1yVjUCnDAtElqiFRlQ/k0LgYkjKDY7LfciZl9uJRl0OSYeX/qG2 +tRW09tOpgQKBgBSGkpM3RN/MRayfBtmZvYjVWh3yjkI2GbHA1jj1g6IebLB9SnfL +WbXJErCj1U+wvoPf5hfBc7m+jRgD3Eo86YXibQyZfY5pFIh9q7Ll5CQl5hj4zc4Y +b16sFR+xQ1Q9Pcd+BuBWmSz5JOE/qcF869dthgkGhnfVLt/OQzqZluZRAoGAXQ09 +nT0TkmKIvlza5Af/YbTqEpq8mlBDhTYXPlWCD4+qvMWpBII1rSSBtftgcgca9XLB +MXmRMbqtQeRtg4u7dishZVh1MeP7vbHsNLppUQT9Ol6lFPsd2xUpJDc6BkFat62d +Xjr3iWNPC9E9nhPPdCNBv7reX7q81obpeXFMXgECgYEAmk2Qlus3OV0tfoNRqNpe +Mb0teduf2+h3xaI1XDIzPVtZF35ELY/RkAHlmWRT4PCdR0zXDidE67L6XdJyecSt +FdOUH8z5qUraVVebRFvJqf/oGsXc4+ex1ZKUTbY0wqY1y9E39yvB3MaTmZFuuqk8 +f3cg+fr8aou7pr9SHhJlZCU= +-----END PRIVATE KEY----- diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/resources/publicKey.pem b/integration-tests/smallrye-jwt-token-propagation/src/main/resources/publicKey.pem new file mode 100644 index 0000000000000..6dc936fca3485 --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/resources/publicKey.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEq +Fyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwR +TYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5e +UF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9 +AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYn +sIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9x +nQIDAQAB +-----END PUBLIC KEY----- diff --git a/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java b/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java new file mode 100644 index 0000000000000..f2fbe961d02cb --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java @@ -0,0 +1,142 @@ +package io.quarkus.it.keycloak; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.RolesRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.util.JsonSerialization; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import io.restassured.RestAssured; + +public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycleManager { + + private static final String KEYCLOAK_SERVER_URL = System.getProperty("keycloak.url", "http://localhost:8180/auth"); + private static final String KEYCLOAK_REALM = "quarkus"; + + @Override + public Map start() { + + RealmRepresentation realm = createRealm(KEYCLOAK_REALM); + realm.setRevokeRefreshToken(true); + realm.setRefreshTokenMaxReuse(0); + realm.setAccessTokenLifespan(3); + + realm.getClients().add(createClient("quarkus-app")); + realm.getUsers().add(createUser("alice", "user")); + realm.getUsers().add(createUser("bob", "user")); + + try { + RestAssured + .given() + .auth().oauth2(getAdminAccessToken()) + .contentType("application/json") + .body(JsonSerialization.writeValueAsBytes(realm)) + .when() + .post(KEYCLOAK_SERVER_URL + "/admin/realms").then() + .statusCode(201); + } catch (IOException e) { + throw new RuntimeException(e); + } + return Collections.emptyMap(); + } + + private static String getAdminAccessToken() { + return RestAssured + .given() + .param("grant_type", "password") + .param("username", "admin") + .param("password", "admin") + .param("client_id", "admin-cli") + .when() + .post(KEYCLOAK_SERVER_URL + "/realms/master/protocol/openid-connect/token") + .as(AccessTokenResponse.class).getToken(); + } + + private static RealmRepresentation createRealm(String name) { + RealmRepresentation realm = new RealmRepresentation(); + + realm.setRealm(name); + realm.setEnabled(true); + realm.setUsers(new ArrayList<>()); + realm.setClients(new ArrayList<>()); + realm.setAccessTokenLifespan(3); + + RolesRepresentation roles = new RolesRepresentation(); + List realmRoles = new ArrayList<>(); + + roles.setRealm(realmRoles); + realm.setRoles(roles); + + realm.getRoles().getRealm().add(new RoleRepresentation("user", null, false)); + realm.getRoles().getRealm().add(new RoleRepresentation("admin", null, false)); + + return realm; + } + + private static ClientRepresentation createClient(String clientId) { + ClientRepresentation client = new ClientRepresentation(); + + client.setClientId(clientId); + client.setPublicClient(false); + client.setSecret("secret"); + client.setDirectAccessGrantsEnabled(true); + client.setServiceAccountsEnabled(true); + client.setEnabled(true); + + return client; + } + + private static UserRepresentation createUser(String username, String... realmRoles) { + UserRepresentation user = new UserRepresentation(); + + user.setUsername(username); + user.setEnabled(true); + user.setCredentials(new ArrayList<>()); + user.setRealmRoles(Arrays.asList(realmRoles)); + user.setEmail(username + "@gmail.com"); + + CredentialRepresentation credential = new CredentialRepresentation(); + + credential.setType(CredentialRepresentation.PASSWORD); + credential.setValue(username); + credential.setTemporary(false); + + user.getCredentials().add(credential); + + return user; + } + + @Override + public void stop() { + + RestAssured + .given() + .auth().oauth2(getAdminAccessToken()) + .when() + .delete(KEYCLOAK_SERVER_URL + "/admin/realms/" + KEYCLOAK_REALM).then().statusCode(204); + } + + public static String getAccessToken(String userName) { + return RestAssured + .given() + .param("grant_type", "password") + .param("username", userName) + .param("password", userName) + .param("client_id", "quarkus-app") + .param("client_secret", "secret") + .when() + .post(KEYCLOAK_SERVER_URL + "/realms/" + KEYCLOAK_REALM + "/protocol/openid-connect/token") + .as(AccessTokenResponse.class).getToken(); + } +} diff --git a/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationInGraalITCase.java b/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationInGraalITCase.java new file mode 100644 index 0000000000000..01ee8ab1acbd6 --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationInGraalITCase.java @@ -0,0 +1,7 @@ +package io.quarkus.it.keycloak; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +public class OidcTokenPropagationInGraalITCase extends OidcTokenPropagationTest { +} diff --git a/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java b/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java new file mode 100644 index 0000000000000..b861a1e0c53f3 --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java @@ -0,0 +1,32 @@ +package io.quarkus.it.keycloak; + +import static org.hamcrest.Matchers.equalTo; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; + +@QuarkusTest +@QuarkusTestResource(KeycloakRealmResourceManager.class) +public class OidcTokenPropagationTest { + + @Test + public void testGetUserNameWithJwtTokenPropagation() { + RestAssured.given().auth().oauth2(KeycloakRealmResourceManager.getAccessToken("alice")) + .when().get("/frontend/jwt-token-propagation") + .then() + .statusCode(200) + .body(equalTo("alice")); + } + + @Test + public void testGetUserNameWithAccessTokenPropagation() { + RestAssured.given().auth().oauth2(KeycloakRealmResourceManager.getAccessToken("alice")) + .when().get("/frontend/access-token-propagation") + .then() + .statusCode(200) + .body(equalTo("alice")); + } +}