diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/token/propagation/TokenPropagationConstants.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/token/propagation/TokenPropagationConstants.java new file mode 100644 index 0000000000000..210aec8998c48 --- /dev/null +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/token/propagation/TokenPropagationConstants.java @@ -0,0 +1,21 @@ +package io.quarkus.oidc.token.propagation; + +public final class TokenPropagationConstants { + + TokenPropagationConstants() { + } + + /** + * System property key that is resolved to true if OIDC auth mechanism should put + * `TokenCredential` into Vert.x duplicated context. + */ + public static final String OIDC_PROPAGATE_TOKEN_CREDENTIAL = "io.quarkus.oidc.runtime." + + "AbstractOidcAuthenticationMechanism.PROPAGATE_TOKEN_CREDENTIAL_WITH_DUPLICATED_CTX"; + /** + * System property key that is resolved to true if JWT auth mechanism should put + * `TokenCredential` into Vert.x duplicated context. + */ + public static final String JWT_PROPAGATE_TOKEN_CREDENTIAL = "io.quarkus.smallrye.jwt.runtime." + + "auth.JWTAuthMechanism.PROPAGATE_TOKEN_CREDENTIAL_WITH_DUPLICATED_CTX"; + +} diff --git a/extensions/oidc-token-propagation-reactive/deployment/src/main/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationReactiveBuildStep.java b/extensions/oidc-token-propagation-reactive/deployment/src/main/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationReactiveBuildStep.java index c36e951020223..e3862e6a1e077 100644 --- a/extensions/oidc-token-propagation-reactive/deployment/src/main/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationReactiveBuildStep.java +++ b/extensions/oidc-token-propagation-reactive/deployment/src/main/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationReactiveBuildStep.java @@ -1,5 +1,8 @@ package io.quarkus.oidc.token.propagation.reactive; +import static io.quarkus.oidc.token.propagation.TokenPropagationConstants.JWT_PROPAGATE_TOKEN_CREDENTIAL; +import static io.quarkus.oidc.token.propagation.TokenPropagationConstants.OIDC_PROPAGATE_TOKEN_CREDENTIAL; + import java.util.Collection; import java.util.List; import java.util.function.BooleanSupplier; @@ -10,15 +13,19 @@ import org.jboss.jandex.Type; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.SystemPropertyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.oidc.token.propagation.AccessToken; import io.quarkus.rest.client.reactive.deployment.DotNames; import io.quarkus.rest.client.reactive.deployment.RegisterProviderAnnotationInstanceBuildItem; +import io.quarkus.runtime.configuration.ConfigurationException; @BuildSteps(onlyIf = OidcTokenPropagationReactiveBuildStep.IsEnabled.class) public class OidcTokenPropagationReactiveBuildStep { @@ -51,6 +58,22 @@ void registerProvider(BuildProducer additionalBeans, } + @BuildStep(onlyIf = IsEnabledDuringAuth.class) + SystemPropertyBuildItem activateTokenCredentialPropagationViaDuplicatedContext(Capabilities capabilities) { + if (capabilities.isPresent(Capability.OIDC)) { + return new SystemPropertyBuildItem(OIDC_PROPAGATE_TOKEN_CREDENTIAL, "true"); + } + + if (capabilities.isPresent(Capability.JWT)) { + return new SystemPropertyBuildItem(JWT_PROPAGATE_TOKEN_CREDENTIAL, "true"); + } + + throw new ConfigurationException( + "Configuration property 'quarkus.oidc-token-propagation-reactive.enabled-during-authentication' is set to " + + "'true', however this configuration property is only supported when either 'quarkus-oidc' or " + + "'quarkus-smallrye-jwt' extensions are present."); + } + public static class IsEnabled implements BooleanSupplier { OidcTokenPropagationReactiveBuildTimeConfig config; @@ -58,4 +81,12 @@ public boolean getAsBoolean() { return config.enabled; } } + + public static class IsEnabledDuringAuth implements BooleanSupplier { + OidcTokenPropagationReactiveBuildTimeConfig config; + + public boolean getAsBoolean() { + return config.enabledDuringAuthentication; + } + } } diff --git a/extensions/oidc-token-propagation-reactive/deployment/src/main/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationReactiveBuildTimeConfig.java b/extensions/oidc-token-propagation-reactive/deployment/src/main/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationReactiveBuildTimeConfig.java index 47f086a3850ba..b372f0933c5a0 100644 --- a/extensions/oidc-token-propagation-reactive/deployment/src/main/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationReactiveBuildTimeConfig.java +++ b/extensions/oidc-token-propagation-reactive/deployment/src/main/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationReactiveBuildTimeConfig.java @@ -13,4 +13,18 @@ public class OidcTokenPropagationReactiveBuildTimeConfig { */ @ConfigItem(defaultValue = "true") public boolean enabled; + + /** + * Whether the token propagation is enabled during the `SecurityIdentity` augmentation. + * + * For example, you may need to use a REST client from `SecurityIdentityAugmentor` + * to propagate the current token to acquire additional roles for the `SecurityIdentity`. + * + * Note, this feature relies on a duplicated context. More information about Vert.x duplicated + * context can be found in xref:duplicated-context[this guide]. + * + * @asciidoclet + */ + @ConfigItem(defaultValue = "false") + public boolean enabledDuringAuthentication; } diff --git a/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/FrontendResource.java b/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/FrontendResource.java index c0b32fbd7bfb6..a5a3d0ee9fe97 100644 --- a/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/FrontendResource.java +++ b/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/FrontendResource.java @@ -8,6 +8,8 @@ import org.eclipse.microprofile.jwt.JsonWebToken; import org.eclipse.microprofile.rest.client.inject.RestClient; +import io.quarkus.security.identity.CurrentIdentityAssociation; + @Path("/frontend") public class FrontendResource { @Inject @@ -17,10 +19,24 @@ public class FrontendResource { @Inject JsonWebToken jwt; + @Inject + CurrentIdentityAssociation identityAssociation; + @GET @Path("token-propagation") @RolesAllowed("admin") public String userNameTokenPropagation() { + return getResponseWithExchangedUsername(); + } + + @GET + @Path("token-propagation-with-augmentor") + @RolesAllowed("tester") // tester role is granted by SecurityIdentityAugmentor + public String userNameTokenPropagationWithSecIdentityAugmentor() { + return getResponseWithExchangedUsername(); + } + + private String getResponseWithExchangedUsername() { if ("alice".equals(jwt.getName())) { return "Token issued to " + jwt.getName() + " has been exchanged, new user name: " + accessTokenPropagationService.getUserName(); diff --git a/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationWithSecurityIdentityAugmentorLazyAuthTest.java b/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationWithSecurityIdentityAugmentorLazyAuthTest.java new file mode 100644 index 0000000000000..9cf1809214436 --- /dev/null +++ b/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationWithSecurityIdentityAugmentorLazyAuthTest.java @@ -0,0 +1,55 @@ +package io.quarkus.oidc.token.propagation.reactive; + +import static io.quarkus.oidc.token.propagation.reactive.RolesSecurityIdentityAugmentor.SUPPORTED_USER; +import static org.hamcrest.Matchers.equalTo; + +import java.util.Set; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.oidc.server.OidcWiremockTestResource; +import io.restassured.RestAssured; + +@QuarkusTestResource(OidcWiremockTestResource.class) +public class OidcTokenPropagationWithSecurityIdentityAugmentorLazyAuthTest { + + private static Class[] testClasses = { + FrontendResource.class, + ProtectedResource.class, + AccessTokenPropagationService.class, + RolesResource.class, + RolesService.class, + RolesSecurityIdentityAugmentor.class + }; + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(testClasses) + .addAsResource("application.properties") + .addAsResource( + new StringAsset("quarkus.oidc-token-propagation-reactive.enabled-during-authentication=true\n" + + "quarkus.rest-client.\"roles\".uri=http://localhost:8081/roles\n" + + "quarkus.http.auth.proactive=false\n"), + "META-INF/microprofile-config.properties")); + + @Test + public void testGetUserNameWithTokenPropagation() { + // request only succeeds if SecurityIdentityAugmentor managed to acquire 'tester' role for user 'alice' + // and that is only possible if access token is propagated during augmentation + RestAssured.given().auth().oauth2(getBearerAccessToken()) + .when().get("/frontend/token-propagation-with-augmentor") + .then() + .statusCode(200) + .body(equalTo("Token issued to alice has been exchanged, new user name: bob")); + } + + public String getBearerAccessToken() { + return OidcWiremockTestResource.getAccessToken(SUPPORTED_USER, Set.of("admin")); + } + +} diff --git a/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationWithSecurityIdentityAugmentorTest.java b/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationWithSecurityIdentityAugmentorTest.java new file mode 100644 index 0000000000000..e918f9f04d6af --- /dev/null +++ b/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationWithSecurityIdentityAugmentorTest.java @@ -0,0 +1,54 @@ +package io.quarkus.oidc.token.propagation.reactive; + +import static io.quarkus.oidc.token.propagation.reactive.RolesSecurityIdentityAugmentor.SUPPORTED_USER; +import static org.hamcrest.Matchers.equalTo; + +import java.util.Set; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.oidc.server.OidcWiremockTestResource; +import io.restassured.RestAssured; + +@QuarkusTestResource(OidcWiremockTestResource.class) +public class OidcTokenPropagationWithSecurityIdentityAugmentorTest { + + private static Class[] testClasses = { + FrontendResource.class, + ProtectedResource.class, + AccessTokenPropagationService.class, + RolesResource.class, + RolesService.class, + RolesSecurityIdentityAugmentor.class + }; + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(testClasses) + .addAsResource("application.properties") + .addAsResource( + new StringAsset("quarkus.oidc-token-propagation-reactive.enabled-during-authentication=true\n" + + "quarkus.rest-client.\"roles\".uri=http://localhost:8081/roles\n"), + "META-INF/microprofile-config.properties")); + + @Test + public void testGetUserNameWithTokenPropagation() { + // request only succeeds if SecurityIdentityAugmentor managed to acquire 'tester' role for user 'alice' + // and that is only possible if access token is propagated during augmentation + RestAssured.given().auth().oauth2(getBearerAccessToken()) + .when().get("/frontend/token-propagation-with-augmentor") + .then() + .statusCode(200) + .body(equalTo("Token issued to alice has been exchanged, new user name: bob")); + } + + public String getBearerAccessToken() { + return OidcWiremockTestResource.getAccessToken(SUPPORTED_USER, Set.of("admin")); + } + +} diff --git a/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/RolesResource.java b/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/RolesResource.java new file mode 100644 index 0000000000000..80d7167434c0c --- /dev/null +++ b/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/RolesResource.java @@ -0,0 +1,26 @@ +package io.quarkus.oidc.token.propagation.reactive; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.ForbiddenException; + +@Path("/roles") +@Authenticated +public class RolesResource { + + @Inject + JsonWebToken jwt; + + @GET + public String get() { + if ("bob".equals(jwt.getName())) { + return "tester"; + } + throw new ForbiddenException("Only user 'bob' is allowed to request roles"); + } +} diff --git a/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/RolesSecurityIdentityAugmentor.java b/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/RolesSecurityIdentityAugmentor.java new file mode 100644 index 0000000000000..47632c40fd86b --- /dev/null +++ b/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/RolesSecurityIdentityAugmentor.java @@ -0,0 +1,34 @@ +package io.quarkus.oidc.token.propagation.reactive; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.smallrye.mutiny.Uni; + +@ApplicationScoped +public class RolesSecurityIdentityAugmentor implements SecurityIdentityAugmentor { + + static final String SUPPORTED_USER = "alice"; + + @Inject + @RestClient + RolesService rolesService; + + @Override + public Uni augment(SecurityIdentity securityIdentity, + AuthenticationRequestContext authenticationRequestContext) { + if (securityIdentity != null && securityIdentity.getPrincipal() != null + && SUPPORTED_USER.equals(securityIdentity.getPrincipal().getName())) { + return rolesService + .getRole() + .map(role -> QuarkusSecurityIdentity.builder(securityIdentity).addRole(role).build()); + } + return Uni.createFrom().item(securityIdentity); + } +} diff --git a/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/RolesService.java b/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/RolesService.java new file mode 100644 index 0000000000000..eed59904a431d --- /dev/null +++ b/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/RolesService.java @@ -0,0 +1,18 @@ +package io.quarkus.oidc.token.propagation.reactive; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.quarkus.oidc.token.propagation.AccessToken; +import io.smallrye.mutiny.Uni; + +@RegisterRestClient(configKey = "roles") +@AccessToken +@Path("/") +public interface RolesService { + + @GET + Uni getRole(); +} diff --git a/extensions/oidc-token-propagation-reactive/runtime/src/main/java/io/quarkus/oidc/token/propagation/reactive/AccessTokenRequestReactiveFilter.java b/extensions/oidc-token-propagation-reactive/runtime/src/main/java/io/quarkus/oidc/token/propagation/reactive/AccessTokenRequestReactiveFilter.java index ed7c1e4981b17..4d54705913d5a 100644 --- a/extensions/oidc-token-propagation-reactive/runtime/src/main/java/io/quarkus/oidc/token/propagation/reactive/AccessTokenRequestReactiveFilter.java +++ b/extensions/oidc-token-propagation-reactive/runtime/src/main/java/io/quarkus/oidc/token/propagation/reactive/AccessTokenRequestReactiveFilter.java @@ -1,5 +1,8 @@ package io.quarkus.oidc.token.propagation.reactive; +import static io.quarkus.oidc.token.propagation.TokenPropagationConstants.JWT_PROPAGATE_TOKEN_CREDENTIAL; +import static io.quarkus.oidc.token.propagation.TokenPropagationConstants.OIDC_PROPAGATE_TOKEN_CREDENTIAL; + import java.util.Collections; import java.util.Optional; import java.util.function.Consumer; @@ -25,12 +28,16 @@ import io.quarkus.oidc.client.runtime.DisabledOidcClientException; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.security.credential.TokenCredential; +import io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle; import io.smallrye.mutiny.Uni; +import io.vertx.core.Vertx; @Priority(Priorities.AUTHENTICATION) public class AccessTokenRequestReactiveFilter implements ResteasyReactiveClientRequestFilter { private static final Logger LOG = Logger.getLogger(AccessTokenRequestReactiveFilter.class); private static final String BEARER_SCHEME_WITH_SPACE = "Bearer "; + private static final String ERROR_MSG = "OIDC Token Propagation Reactive requires a safe (isolated) Vert.x sub-context because configuration property 'quarkus.oidc-token-propagation-reactive.enabled-during-authentication' has been set to true, but the current context hasn't been flagged as such."; + private final boolean enabledDuringAuthentication; @Inject Instance accessToken; @@ -45,6 +52,11 @@ public class AccessTokenRequestReactiveFilter implements ResteasyReactiveClientR OidcClient exchangeTokenClient; String exchangeTokenProperty; + public AccessTokenRequestReactiveFilter() { + this.enabledDuringAuthentication = Boolean.getBoolean(OIDC_PROPAGATE_TOKEN_CREDENTIAL) + || Boolean.getBoolean(JWT_PROPAGATE_TOKEN_CREDENTIAL); + } + @PostConstruct public void initExchangeTokenClient() { if (exchangeToken) { @@ -73,7 +85,7 @@ public void filter(ResteasyReactiveClientRequestContext requestContext) { requestContext.suspend(); - exchangeToken(accessToken.get().getToken()).subscribe().with(new Consumer<>() { + exchangeToken(getAccessToken().getToken()).subscribe().with(new Consumer<>() { @Override public void accept(String token) { propagateToken(requestContext, token); @@ -94,7 +106,7 @@ public void accept(Throwable t) { } }); } else { - propagateToken(requestContext, accessToken.get().getToken()); + propagateToken(requestContext, getAccessToken().getToken()); } } else { abortRequest(requestContext); @@ -111,6 +123,19 @@ public void propagateToken(ResteasyReactiveClientRequestContext requestContext, } protected boolean verifyTokenInstance(ResteasyReactiveClientRequestContext requestContext) { + if (enabledDuringAuthentication) { + // TokenCredential cannot be accessed from CDI during authentication process + TokenCredential tokenCredential = getTokenCredentialFromContext(); + if (tokenCredential != null) { + if (tokenCredential.getToken() == null) { + LOG.debugf("Propagated access token is null, aborting the request with HTTP 401 error"); + return false; + } + // use propagated access token + return true; + } + // this means authentication is already done, therefore we use CDI + } if (!accessToken.isResolvable()) { LOG.debugf("Access token is not injected, aborting the request with HTTP 401 error"); return false; @@ -126,6 +151,22 @@ protected boolean verifyTokenInstance(ResteasyReactiveClientRequestContext reque return true; } + private TokenCredential getAccessToken() { + if (enabledDuringAuthentication) { + TokenCredential tokenCredential = getTokenCredentialFromContext(); + if (tokenCredential != null) { + return tokenCredential; + } + // this means auth is already done, therefore let's use CDI + } + return accessToken.get(); + } + + private static TokenCredential getTokenCredentialFromContext() { + VertxContextSafetyToggle.validateContextIfExists(ERROR_MSG, ERROR_MSG); + return Vertx.currentContext().getLocal(TokenCredential.class.getName()); + } + private Uni exchangeToken(String token) { return exchangeTokenClient.getTokens(Collections.singletonMap(exchangeTokenProperty, token)) .onItem().transform(t -> t.getAccessToken()); 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 10a38cd4ce61b..9990ca1481ab8 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,13 +1,19 @@ package io.quarkus.oidc.token.propagation.deployment; +import static io.quarkus.oidc.token.propagation.TokenPropagationConstants.JWT_PROPAGATE_TOKEN_CREDENTIAL; +import static io.quarkus.oidc.token.propagation.TokenPropagationConstants.OIDC_PROPAGATE_TOKEN_CREDENTIAL; + import java.util.function.BooleanSupplier; import org.jboss.jandex.DotName; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.builditem.SystemPropertyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.oidc.token.propagation.AccessToken; import io.quarkus.oidc.token.propagation.AccessTokenRequestFilter; @@ -17,6 +23,7 @@ import io.quarkus.oidc.token.propagation.runtime.OidcTokenPropagationConfig; import io.quarkus.restclient.deployment.RestClientAnnotationProviderBuildItem; import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; +import io.quarkus.runtime.configuration.ConfigurationException; @BuildSteps(onlyIf = OidcTokenPropagationBuildStep.IsEnabled.class) public class OidcTokenPropagationBuildStep { @@ -49,6 +56,22 @@ void registerProvider(BuildProducer additionalBeans, } } + @BuildStep(onlyIf = IsEnabledDuringAuth.class) + SystemPropertyBuildItem activateTokenCredentialPropagationViaDuplicatedContext(Capabilities capabilities) { + if (capabilities.isPresent(Capability.OIDC)) { + return new SystemPropertyBuildItem(OIDC_PROPAGATE_TOKEN_CREDENTIAL, "true"); + } + + if (capabilities.isPresent(Capability.JWT)) { + return new SystemPropertyBuildItem(JWT_PROPAGATE_TOKEN_CREDENTIAL, "true"); + } + + throw new ConfigurationException( + "Configuration property 'quarkus.oidc-token-propagation.enabled-during-authentication' is set to " + + "'true', however this configuration property is only supported when either 'quarkus-oidc' or " + + "'quarkus-smallrye-jwt' extensions are present."); + } + public static class IsEnabled implements BooleanSupplier { OidcTokenPropagationBuildTimeConfig config; @@ -56,4 +79,12 @@ public boolean getAsBoolean() { return config.enabled; } } + + public static class IsEnabledDuringAuth implements BooleanSupplier { + OidcTokenPropagationBuildTimeConfig config; + + public boolean getAsBoolean() { + return config.enabledDuringAuthentication; + } + } } diff --git a/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/FrontendResource.java b/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/FrontendResource.java index 1a4ee31674097..5f0a7cd283a52 100644 --- a/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/FrontendResource.java +++ b/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/FrontendResource.java @@ -21,6 +21,17 @@ public class FrontendResource { @Path("token-propagation") @RolesAllowed("admin") public String userNameTokenPropagation() { + return getResponseWithExchangedUsername(); + } + + @GET + @Path("token-propagation-with-augmentor") + @RolesAllowed("tester") // tester role is granted by SecurityIdentityAugmentor + public String userNameTokenPropagationWithSecIdentityAugmentor() { + return getResponseWithExchangedUsername(); + } + + private String getResponseWithExchangedUsername() { if ("alice".equals(jwt.getName())) { return "Token issued to " + jwt.getName() + " has been exchanged, new user name: " + accessTokenPropagationService.getUserName(); diff --git a/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/OidcTokenPropagationWithSecurityIdentityAugmentorLazyAuthTest.java b/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/OidcTokenPropagationWithSecurityIdentityAugmentorLazyAuthTest.java new file mode 100644 index 0000000000000..ce2f329a4a57c --- /dev/null +++ b/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/OidcTokenPropagationWithSecurityIdentityAugmentorLazyAuthTest.java @@ -0,0 +1,54 @@ +package io.quarkus.oidc.token.propagation; + +import static io.quarkus.oidc.token.propagation.RolesSecurityIdentityAugmentor.SUPPORTED_USER; +import static org.hamcrest.Matchers.equalTo; + +import java.util.Set; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.oidc.server.OidcWiremockTestResource; +import io.restassured.RestAssured; + +@QuarkusTestResource(OidcWiremockTestResource.class) +public class OidcTokenPropagationWithSecurityIdentityAugmentorLazyAuthTest { + + private static Class[] testClasses = { + FrontendResource.class, + ProtectedResource.class, + AccessTokenPropagationService.class, + RolesResource.class, + RolesService.class, + RolesSecurityIdentityAugmentor.class + }; + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(testClasses) + .addAsResource("application.properties") + .addAsResource(new StringAsset("quarkus.oidc-token-propagation.enabled-during-authentication=true\n" + + "quarkus.rest-client.\"roles\".uri=http://localhost:8081/roles\n" + + "quarkus.http.auth.proactive=false\n"), + "META-INF/microprofile-config.properties")); + + @Test + public void testGetUserNameWithTokenPropagation() { + // request only succeeds if SecurityIdentityAugmentor managed to acquire 'tester' role for user 'alice' + // and that is only possible if access token is propagated during augmentation + RestAssured.given().auth().oauth2(getBearerAccessToken()) + .when().get("/frontend/token-propagation-with-augmentor") + .then() + .statusCode(200) + .body(equalTo("Token issued to alice has been exchanged, new user name: bob")); + } + + public String getBearerAccessToken() { + return OidcWiremockTestResource.getAccessToken(SUPPORTED_USER, Set.of("admin")); + } + +} diff --git a/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/OidcTokenPropagationWithSecurityIdentityAugmentorTest.java b/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/OidcTokenPropagationWithSecurityIdentityAugmentorTest.java new file mode 100644 index 0000000000000..f0ab3d8b7e874 --- /dev/null +++ b/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/OidcTokenPropagationWithSecurityIdentityAugmentorTest.java @@ -0,0 +1,53 @@ +package io.quarkus.oidc.token.propagation; + +import static io.quarkus.oidc.token.propagation.RolesSecurityIdentityAugmentor.SUPPORTED_USER; +import static org.hamcrest.Matchers.equalTo; + +import java.util.Set; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.oidc.server.OidcWiremockTestResource; +import io.restassured.RestAssured; + +@QuarkusTestResource(OidcWiremockTestResource.class) +public class OidcTokenPropagationWithSecurityIdentityAugmentorTest { + + private static Class[] testClasses = { + FrontendResource.class, + ProtectedResource.class, + AccessTokenPropagationService.class, + RolesResource.class, + RolesService.class, + RolesSecurityIdentityAugmentor.class + }; + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(testClasses) + .addAsResource("application.properties") + .addAsResource(new StringAsset("quarkus.oidc-token-propagation.enabled-during-authentication=true\n" + + "quarkus.rest-client.\"roles\".uri=http://localhost:8081/roles\n"), + "META-INF/microprofile-config.properties")); + + @Test + public void testGetUserNameWithTokenPropagation() { + // request only succeeds if SecurityIdentityAugmentor managed to acquire 'tester' role for user 'alice' + // and that is only possible if access token is propagated during augmentation + RestAssured.given().auth().oauth2(getBearerAccessToken()) + .when().get("/frontend/token-propagation-with-augmentor") + .then() + .statusCode(200) + .body(equalTo("Token issued to alice has been exchanged, new user name: bob")); + } + + public String getBearerAccessToken() { + return OidcWiremockTestResource.getAccessToken(SUPPORTED_USER, Set.of("admin")); + } + +} diff --git a/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/RolesResource.java b/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/RolesResource.java new file mode 100644 index 0000000000000..2fa2ddd4404fd --- /dev/null +++ b/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/RolesResource.java @@ -0,0 +1,26 @@ +package io.quarkus.oidc.token.propagation; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.ForbiddenException; + +@Path("/roles") +@Authenticated +public class RolesResource { + + @Inject + JsonWebToken jwt; + + @GET + public String get() { + if ("bob".equals(jwt.getName())) { + return "tester"; + } + throw new ForbiddenException("Only user 'bob' is allowed to request roles"); + } +} diff --git a/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/RolesSecurityIdentityAugmentor.java b/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/RolesSecurityIdentityAugmentor.java new file mode 100644 index 0000000000000..03b97c253fe52 --- /dev/null +++ b/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/RolesSecurityIdentityAugmentor.java @@ -0,0 +1,35 @@ +package io.quarkus.oidc.token.propagation; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.smallrye.mutiny.Uni; + +@ApplicationScoped +public class RolesSecurityIdentityAugmentor implements SecurityIdentityAugmentor { + + static final String SUPPORTED_USER = "alice"; + + @Inject + @RestClient + RolesService rolesService; + + @Override + public Uni augment(SecurityIdentity securityIdentity, + AuthenticationRequestContext authenticationRequestContext) { + if (securityIdentity != null && securityIdentity.getPrincipal() != null + && SUPPORTED_USER.equals(securityIdentity.getPrincipal().getName())) { + return authenticationRequestContext.runBlocking(() -> { + String role = rolesService.getRole(); + return QuarkusSecurityIdentity.builder(securityIdentity).addRole(role).build(); + }); + } + return Uni.createFrom().item(securityIdentity); + } +} diff --git a/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/RolesService.java b/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/RolesService.java new file mode 100644 index 0000000000000..5a2047e9dcff5 --- /dev/null +++ b/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/RolesService.java @@ -0,0 +1,15 @@ +package io.quarkus.oidc.token.propagation; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@RegisterRestClient(configKey = "roles") +@AccessToken +@Path("/") +public interface RolesService { + + @GET + String getRole(); +} 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 6f08d7a51d887..ffffd3cb145a8 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 @@ -1,5 +1,8 @@ package io.quarkus.oidc.token.propagation; +import static io.quarkus.oidc.token.propagation.TokenPropagationConstants.JWT_PROPAGATE_TOKEN_CREDENTIAL; +import static io.quarkus.oidc.token.propagation.TokenPropagationConstants.OIDC_PROPAGATE_TOKEN_CREDENTIAL; + import java.io.IOException; import java.util.Collections; import java.util.Optional; @@ -19,11 +22,16 @@ import io.quarkus.oidc.token.propagation.runtime.AbstractTokenRequestFilter; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.security.credential.TokenCredential; +import io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle; +import io.vertx.core.Vertx; public class AccessTokenRequestFilter extends AbstractTokenRequestFilter { // note: We can't use constructor injection for these fields because they are registered by RESTEasy // which doesn't know about CDI at the point of registration + private static final String ERROR_MSG = "OIDC Token Propagation requires a safe (isolated) Vert.x sub-context because configuration property 'quarkus.oidc-token-propagation.enabled-during-authentication' has been set to true, but the current context hasn't been flagged as such."; + private final boolean enabledDuringAuthentication; + @Inject Instance accessToken; @@ -37,6 +45,11 @@ public class AccessTokenRequestFilter extends AbstractTokenRequestFilter { OidcClient exchangeTokenClient; String exchangeTokenProperty; + public AccessTokenRequestFilter() { + this.enabledDuringAuthentication = Boolean.getBoolean(OIDC_PROPAGATE_TOKEN_CREDENTIAL) + || Boolean.getBoolean(JWT_PROPAGATE_TOKEN_CREDENTIAL); + } + @PostConstruct public void initExchangeTokenClient() { if (exchangeToken) { @@ -60,8 +73,12 @@ public void initExchangeTokenClient() { @Override public void filter(ClientRequestContext requestContext) throws IOException { - if (verifyTokenInstance(requestContext, accessToken)) { - propagateToken(requestContext, exchangeTokenIfNeeded(accessToken.get().getToken())); + if (acquireTokenCredentialFromCtx(requestContext)) { + propagateToken(requestContext, exchangeTokenIfNeeded(getTokenCredentialFromContext().getToken())); + } else { + if (verifyTokenInstance(requestContext, accessToken)) { + propagateToken(requestContext, exchangeTokenIfNeeded(accessToken.get().getToken())); + } } } @@ -74,4 +91,24 @@ private String exchangeTokenIfNeeded(String token) { return token; } } + + private boolean acquireTokenCredentialFromCtx(ClientRequestContext requestContext) { + if (enabledDuringAuthentication) { + TokenCredential tokenCredential = getTokenCredentialFromContext(); + if (tokenCredential != null) { + if (tokenCredential.getToken() == null) { + abortRequest(requestContext); + } else { + return true; + } + } + // this means auth is already done, and we need to use CDI + } + return false; + } + + private static TokenCredential getTokenCredentialFromContext() { + VertxContextSafetyToggle.validateContextIfExists(ERROR_MSG, ERROR_MSG); + return Vertx.currentContext().getLocal(TokenCredential.class.getName()); + } } 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 index 84c1feda61bae..36e94211fb356 100644 --- 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 @@ -13,4 +13,18 @@ public class OidcTokenPropagationBuildTimeConfig { */ @ConfigItem(defaultValue = "true") public boolean enabled; + + /** + * Whether the token propagation is enabled during the `SecurityIdentity` augmentation. + * + * For example, you may need to use a REST client from `SecurityIdentityAugmentor` + * to propagate the current token to acquire additional roles for the `SecurityIdentity`. + * + * Note, this feature relies on a duplicated context. More information about Vert.x duplicated + * context can be found in xref:duplicated-context[this guide]. + * + * @asciidoclet + */ + @ConfigItem(defaultValue = "false") + public boolean enabledDuringAuthentication; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractOidcAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractOidcAuthenticationMechanism.java index 11176370f2250..d3cec873ebb47 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractOidcAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractOidcAuthenticationMechanism.java @@ -4,18 +4,56 @@ import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.TokenAuthenticationRequest; +import io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils; import io.smallrye.mutiny.Uni; +import io.vertx.core.Vertx; import io.vertx.ext.web.RoutingContext; abstract class AbstractOidcAuthenticationMechanism { + /** + * System property key that is resolved to true if OIDC auth mechanism should put + * `TokenCredential` into Vert.x duplicated context. + */ + private static final String OIDC_PROPAGATE_TOKEN_CREDENTIAL = "io.quarkus.oidc.runtime." + + "AbstractOidcAuthenticationMechanism.PROPAGATE_TOKEN_CREDENTIAL_WITH_DUPLICATED_CTX"; + private static final String ERROR_MSG = "OIDC requires a safe (isolated) Vert.x sub-context for propagation of the '" + + TokenCredential.class.getName() + "', but the current context hasn't been flagged as such."; protected DefaultTenantConfigResolver resolver; + /** + * Propagate {@link TokenCredential} via Vert.X duplicated context if explicitly enabled and request context + * can not be activated. + */ + private final boolean propagateTokenCredentialWithDuplicatedCtx; private HttpAuthenticationMechanism parent; + AbstractOidcAuthenticationMechanism() { + // we use system property in order to keep this option internal and avoid introducing SPI + this.propagateTokenCredentialWithDuplicatedCtx = Boolean + .getBoolean(OIDC_PROPAGATE_TOKEN_CREDENTIAL); + } + protected Uni authenticate(IdentityProviderManager identityProviderManager, RoutingContext context, TokenCredential token) { context.put(HttpAuthenticationMechanism.class.getName(), parent); + + if (propagateTokenCredentialWithDuplicatedCtx) { + // during authentication TokenCredential is not accessible via CDI, thus we put it to the duplicated context + VertxContextSafetyToggle.validateContextIfExists(ERROR_MSG, ERROR_MSG); + final var ctx = Vertx.currentContext(); + ctx.putLocal(TokenCredential.class.getName(), token); + return identityProviderManager + .authenticate(HttpSecurityUtils.setRoutingContextAttribute(new TokenAuthenticationRequest(token), context)) + .invoke(new Runnable() { + @Override + public void run() { + // remove as we recommend to acquire TokenCredential via CDI + ctx.removeLocal(TokenCredential.class.getName()); + } + }); + } + return identityProviderManager.authenticate(HttpSecurityUtils.setRoutingContextAttribute( new TokenAuthenticationRequest(token), context)); } 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 28b518ad9ff20..d5a8a43f53590 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 @@ -12,10 +12,12 @@ 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; import io.quarkus.security.identity.request.TokenAuthenticationRequest; +import io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle; import io.quarkus.vertx.http.runtime.security.ChallengeData; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport; @@ -23,6 +25,7 @@ import io.smallrye.jwt.auth.AbstractBearerTokenExtractor; import io.smallrye.jwt.auth.principal.JWTAuthContextInfo; import io.smallrye.mutiny.Uni; +import io.vertx.core.Vertx; import io.vertx.core.http.Cookie; import io.vertx.ext.web.RoutingContext; @@ -31,16 +34,27 @@ */ @ApplicationScoped public class JWTAuthMechanism implements HttpAuthenticationMechanism { + private static final String ERROR_MSG = "SmallRye JWT requires a safe (isolated) Vert.x sub-context for propagation " + + "of the '" + TokenCredential.class.getName() + "', but the current context hasn't been flagged as such."; protected static final String COOKIE_HEADER = "Cookie"; protected static final String AUTHORIZATION_HEADER = "Authorization"; protected static final String BEARER = "Bearer"; + /** + * Propagate {@link TokenCredential} via Vert.X duplicated context if explicitly enabled and request context + * can not be activated. + */ + private final boolean propagateTokenCredentialWithDuplicatedCtx; @Inject private JWTAuthContextInfo authContextInfo; private final boolean silent; public JWTAuthMechanism(SmallRyeJwtConfig config) { this.silent = config == null ? false : config.silent; + // we use system property in order to keep this option internal and avoid introducing SPI + this.propagateTokenCredentialWithDuplicatedCtx = Boolean + .getBoolean("io.quarkus.smallrye.jwt.runtime.auth.JWTAuthMechanism." + + "PROPAGATE_TOKEN_CREDENTIAL_WITH_DUPLICATED_CTX"); } @Override @@ -49,6 +63,26 @@ public Uni authenticate(RoutingContext context, String jwtToken = new VertxBearerTokenExtractor(authContextInfo, context).getBearerToken(); if (jwtToken != null) { context.put(HttpAuthenticationMechanism.class.getName(), this); + + if (propagateTokenCredentialWithDuplicatedCtx) { + // during authentication TokenCredential is not accessible via CDI, + // thus we put it to the duplicated context + VertxContextSafetyToggle.validateContextIfExists(ERROR_MSG, ERROR_MSG); + final var ctx = Vertx.currentContext(); + final var token = new JsonWebTokenCredential(jwtToken); + ctx.putLocal(TokenCredential.class.getName(), token); + return identityProviderManager + .authenticate(HttpSecurityUtils.setRoutingContextAttribute( + new TokenAuthenticationRequest(token), context)) + .invoke(new Runnable() { + @Override + public void run() { + // remove as we recommend to acquire TokenCredential via CDI + ctx.removeLocal(TokenCredential.class.getName()); + } + }); + } + return identityProviderManager .authenticate(HttpSecurityUtils.setRoutingContextAttribute( new TokenAuthenticationRequest(new JsonWebTokenCredential(jwtToken)), context)); 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 index ca54f476f4ab9..fd97ad340a00d 100644 --- 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 @@ -37,6 +37,14 @@ public String userNameJwtTokenPropagation() { return jwtTokenPropagationService.getUserName(); } + @GET + @Path("jwt-token-propagation-with-augmentation") + @RolesAllowed("tester") + public String userNameJwtTokenPropagationWithSecIdentityAugmentation() { + checkIssuerAndAudience(); + return jwtTokenPropagationService.getUserName(); + } + @GET @Path("access-token-propagation") @RolesAllowed("user") diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/RolesResource.java b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/RolesResource.java new file mode 100644 index 0000000000000..24a18dd122cdd --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/RolesResource.java @@ -0,0 +1,26 @@ +package io.quarkus.it.keycloak; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.ForbiddenException; + +@Path("/roles") +@Authenticated +public class RolesResource { + + @Inject + JsonWebToken jwt; + + @GET + public String get() { + if ("alice".equals(jwt.getName())) { + return "tester"; + } + throw new ForbiddenException("Only user 'bob' is allowed to request roles"); + } +} diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/RolesSecurityIdentityAugmentor.java b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/RolesSecurityIdentityAugmentor.java new file mode 100644 index 0000000000000..43c7b418bb99a --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/RolesSecurityIdentityAugmentor.java @@ -0,0 +1,42 @@ +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class RolesSecurityIdentityAugmentor implements SecurityIdentityAugmentor { + + public static final String USE_SEC_IDENTITY_AUGMENTOR = RolesSecurityIdentityAugmentor.class.getName(); + + @Inject + @RestClient + RolesService rolesService; + + @Inject + RoutingContext routingContext; + + @Override + public Uni augment(SecurityIdentity securityIdentity, + AuthenticationRequestContext authenticationRequestContext) { + if (securityIdentity != null && securityIdentity.getPrincipal() != null + && "alice".equals(securityIdentity.getPrincipal().getName())) { + boolean augmentIdentity = Boolean.parseBoolean(routingContext.request().getHeader(USE_SEC_IDENTITY_AUGMENTOR)); + if (augmentIdentity) { + return authenticationRequestContext.runBlocking(() -> { + String role = rolesService.getRole(); + return QuarkusSecurityIdentity.builder(securityIdentity).addRole(role).build(); + }); + } + } + return Uni.createFrom().item(securityIdentity); + } +} diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/RolesService.java b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/RolesService.java new file mode 100644 index 0000000000000..6e79837657bca --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/RolesService.java @@ -0,0 +1,17 @@ +package io.quarkus.it.keycloak; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.quarkus.oidc.token.propagation.AccessToken; + +@RegisterRestClient(configKey = "roles") +@AccessToken +@Path("/") +public interface RolesService { + + @GET + String getRole(); +} 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 index 82cff223198db..d3396f08767f1 100644 --- a/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties @@ -13,3 +13,7 @@ smallrye.jwt.new-token.override-matching-claims=true quarkus.http.auth.proactive=false quarkus.native.additional-build-args=-H:IncludeResources=publicKey.pem + +# augment security identity on demand +quarkus.rest-client."roles".uri=http://localhost:8081/roles +quarkus.oidc-token-propagation.enabled-during-authentication=true 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 index 03c22f825c57d..fce76b77d1d78 100644 --- 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 @@ -1,5 +1,6 @@ package io.quarkus.it.keycloak; +import static io.quarkus.it.keycloak.RolesSecurityIdentityAugmentor.USE_SEC_IDENTITY_AUGMENTOR; import static org.hamcrest.Matchers.equalTo; import org.junit.jupiter.api.Test; @@ -22,6 +23,16 @@ public void testGetUserNameWithJwtTokenPropagation() { .body(equalTo("alice")); } + @Test + public void testGetUserNameWithJwtTokenPropagationAndAugmentedIdentity() { + RestAssured.given().auth().oauth2(KeycloakRealmResourceManager.getAccessToken("alice")) + .header(USE_SEC_IDENTITY_AUGMENTOR, Boolean.TRUE) + .when().get("/frontend/jwt-token-propagation-with-augmentation") + .then() + .statusCode(200) + .body(equalTo("alice")); + } + @Test public void testGetUserNameWithAccessTokenPropagation() { RestAssured.given().auth().oauth2(KeycloakRealmResourceManager.getAccessToken("alice"))