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 super SecurityIdentity> 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"));
+ }
+}