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..92a09850c8e19 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 @@ -10,15 +10,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 +55,17 @@ void registerProvider(BuildProducer additionalBeans, } + @BuildStep(onlyIf = IsEnabledDuringAuth.class) + SystemPropertyBuildItem activateTokenCredentialPropagationViaDuplicatedContext(Capabilities capabilities) { + if (capabilities.isPresent(Capability.OIDC)) { + return new SystemPropertyBuildItem("quarkus.oidc.propagate-token-credential-with-duplicated-context", "true"); + } + + throw new ConfigurationException( + "Configuration property 'quarkus.oidc-token-propagation-reactive.enable-during-auth' is set to 'true', " + + "however this configuration property is only supported when Quarkus OIDC extension is present."); + } + public static class IsEnabled implements BooleanSupplier { OidcTokenPropagationReactiveBuildTimeConfig config; @@ -58,4 +73,12 @@ public boolean getAsBoolean() { return config.enabled; } } + + public static class IsEnabledDuringAuth implements BooleanSupplier { + OidcTokenPropagationReactiveBuildTimeConfig config; + + public boolean getAsBoolean() { + return config.enableDuringAuth; + } + } } 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..5f1da5fa29551 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,17 @@ public class OidcTokenPropagationReactiveBuildTimeConfig { */ @ConfigItem(defaultValue = "true") public boolean enabled; + + /** + * If the OIDC Token Reactive Propagation is enabled during authentication, such as when you need + * to use a REST client from {@link io.quarkus.security.identity.SecurityIdentityAugmentor}. + * By default, access token propagation is not enabled during authentication, because + * current implementation requires storing of local data on Vert.X duplicated context in a safe manner. + * It is safe to enable this option in most cases, but please revise how your extensions work + * with Vert.X context. + * + * This configuration options is only supported when used together with the Quarkus OIDC extension. + */ + @ConfigItem(defaultValue = "false") + public boolean enableDuringAuth; } 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/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..83ce57762b513 --- /dev/null +++ b/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationWithSecurityIdentityAugmentorTest.java @@ -0,0 +1,53 @@ +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.enable-during-auth=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..c5c59f837002f 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 @@ -25,12 +25,15 @@ 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.enable-during-auth' has been set to true, but the current context hasn't been flagged as such."; @Inject Instance accessToken; @@ -41,6 +44,9 @@ public class AccessTokenRequestReactiveFilter implements ResteasyReactiveClientR @Inject @ConfigProperty(name = "quarkus.oidc-token-propagation-reactive.exchange-token") boolean exchangeToken; + @Inject + @ConfigProperty(name = "quarkus.oidc-token-propagation-reactive.enable-during-auth", defaultValue = "false") + boolean enableDuringAuthentication; OidcClient exchangeTokenClient; String exchangeTokenProperty; @@ -73,7 +79,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 +100,7 @@ public void accept(Throwable t) { } }); } else { - propagateToken(requestContext, accessToken.get().getToken()); + propagateToken(requestContext, getAccessToken().getToken()); } } else { abortRequest(requestContext); @@ -111,6 +117,11 @@ public void propagateToken(ResteasyReactiveClientRequestContext requestContext, } protected boolean verifyTokenInstance(ResteasyReactiveClientRequestContext requestContext) { + if (enableDuringAuthentication && !Arc.container().requestContext().isActive()) { + // it is possible request context is not active when this filter is used during authentication + TokenCredential tokenCredential = getTokenCredentialFromContext(); + return tokenCredential != null; + } if (!accessToken.isResolvable()) { LOG.debugf("Access token is not injected, aborting the request with HTTP 401 error"); return false; @@ -126,6 +137,18 @@ protected boolean verifyTokenInstance(ResteasyReactiveClientRequestContext reque return true; } + private TokenCredential getAccessToken() { + if (enableDuringAuthentication && !Arc.container().requestContext().isActive()) { + return getTokenCredentialFromContext(); + } + 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..7affd149f5c6b 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 @@ -5,9 +5,12 @@ 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 +20,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 +53,17 @@ void registerProvider(BuildProducer additionalBeans, } } + @BuildStep(onlyIf = IsEnabledDuringAuth.class) + SystemPropertyBuildItem activateTokenCredentialPropagationViaDuplicatedContext(Capabilities capabilities) { + if (capabilities.isPresent(Capability.OIDC)) { + return new SystemPropertyBuildItem("quarkus.oidc.propagate-token-credential-with-duplicated-context", "true"); + } + + throw new ConfigurationException( + "Configuration property 'quarkus.oidc-token-propagation.enable-during-auth' is set to 'true', " + + "however this configuration property is only supported when Quarkus OIDC extension is present."); + } + public static class IsEnabled implements BooleanSupplier { OidcTokenPropagationBuildTimeConfig config; @@ -56,4 +71,12 @@ public boolean getAsBoolean() { return config.enabled; } } + + public static class IsEnabledDuringAuth implements BooleanSupplier { + OidcTokenPropagationBuildTimeConfig config; + + public boolean getAsBoolean() { + return config.enableDuringAuth; + } + } } 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/OidcTokenPropagationWithSecurityIdentityAugmentorTest.java b/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/OidcTokenPropagationWithSecurityIdentityAugmentorTest.java new file mode 100644 index 0000000000000..27eb9ee8fcd3c --- /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.enable-during-auth=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..75fd1ebf71ee4 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 @@ -19,11 +19,15 @@ 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.enable-during-auth' has been set to true, but the current context hasn't been flagged as such."; + @Inject Instance accessToken; @@ -33,6 +37,9 @@ public class AccessTokenRequestFilter extends AbstractTokenRequestFilter { @Inject @ConfigProperty(name = "quarkus.oidc-token-propagation.exchange-token") boolean exchangeToken; + @Inject + @ConfigProperty(name = "quarkus.oidc-token-propagation.enable-during-auth", defaultValue = "false") + boolean enableDuringAuthentication; OidcClient exchangeTokenClient; String exchangeTokenProperty; @@ -60,8 +67,12 @@ public void initExchangeTokenClient() { @Override public void filter(ClientRequestContext requestContext) throws IOException { - if (verifyTokenInstance(requestContext, accessToken)) { - propagateToken(requestContext, exchangeTokenIfNeeded(accessToken.get().getToken())); + if (acquireTokenCredentialFromCtx()) { + propagateToken(requestContext, exchangeTokenIfNeeded(getTokenCredentialFromContext().getToken())); + } else { + if (verifyTokenInstance(requestContext, accessToken)) { + propagateToken(requestContext, exchangeTokenIfNeeded(accessToken.get().getToken())); + } } } @@ -74,4 +85,16 @@ private String exchangeTokenIfNeeded(String token) { return token; } } + + private boolean acquireTokenCredentialFromCtx() { + if (enableDuringAuthentication && !Arc.container().requestContext().isActive()) { + return getTokenCredentialFromContext() != null; + } + 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..d91f17a4d2991 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,17 @@ public class OidcTokenPropagationBuildTimeConfig { */ @ConfigItem(defaultValue = "true") public boolean enabled; + + /** + * If the OIDC Token Propagation is enabled during authentication, such as when you need + * to use a REST client from {@link io.quarkus.security.identity.SecurityIdentityAugmentor}. + * By default, access token propagation is not enabled during authentication, because + * current implementation requires storing of local data on Vert.X duplicated context in a safe manner. + * It is safe to enable this option in most cases, but please revise how your extensions work + * with Vert.X context. + * + * This configuration options is only supported when used together with the Quarkus OIDC extension. + */ + @ConfigItem(defaultValue = "false") + public boolean enableDuringAuth; } 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..f32a174ad9d4b 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 @@ -1,21 +1,49 @@ package io.quarkus.oidc.runtime; +import io.quarkus.arc.Arc; import io.quarkus.security.credential.TokenCredential; 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 { + /** + * Propagate {@link TokenCredential} via Vert.X duplicated context if explicitly enabled and request context + * can not be activated. + */ + private static final boolean PROPAGATE_TOKEN_CREDENTIAL_WITH_DUPLICATED_CTX = Boolean + .getBoolean("quarkus.oidc.propagate-token-credential-with-duplicated-context"); + 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; private HttpAuthenticationMechanism parent; protected Uni authenticate(IdentityProviderManager identityProviderManager, RoutingContext context, TokenCredential token) { context.put(HttpAuthenticationMechanism.class.getName(), parent); + + if (PROPAGATE_TOKEN_CREDENTIAL_WITH_DUPLICATED_CTX && !Arc.container().requestContext().isActive()) { + // 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)); }