From 7e7b523dba92931e56f0011128d61d785409fd7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Wed, 26 Jul 2023 16:28:00 +0200 Subject: [PATCH] Support token propagation during identity augmentation --- ...OidcTokenPropagationReactiveBuildStep.java | 29 ++++++++++ ...kenPropagationReactiveBuildTimeConfig.java | 14 +++++ .../reactive/FrontendResource.java | 16 ++++++ ...SecurityIdentityAugmentorLazyAuthTest.java | 55 +++++++++++++++++++ ...tionWithSecurityIdentityAugmentorTest.java | 54 ++++++++++++++++++ .../propagation/reactive/RolesResource.java | 26 +++++++++ .../RolesSecurityIdentityAugmentor.java | 34 ++++++++++++ .../propagation/reactive/RolesService.java | 18 ++++++ .../AccessTokenRequestReactiveFilter.java | 39 ++++++++++++- .../OidcTokenPropagationBuildStep.java | 29 ++++++++++ .../token/propagation/FrontendResource.java | 11 ++++ ...SecurityIdentityAugmentorLazyAuthTest.java | 54 ++++++++++++++++++ ...tionWithSecurityIdentityAugmentorTest.java | 53 ++++++++++++++++++ .../oidc/token/propagation/RolesResource.java | 26 +++++++++ .../RolesSecurityIdentityAugmentor.java | 35 ++++++++++++ .../oidc/token/propagation/RolesService.java | 15 +++++ .../propagation/AccessTokenRequestFilter.java | 35 +++++++++++- .../OidcTokenPropagationBuildTimeConfig.java | 14 +++++ .../AbstractOidcAuthenticationMechanism.java | 27 +++++++++ .../jwt/runtime/auth/JWTAuthMechanism.java | 31 +++++++++++ .../quarkus/it/keycloak/FrontendResource.java | 8 +++ .../io/quarkus/it/keycloak/RolesResource.java | 26 +++++++++ .../RolesSecurityIdentityAugmentor.java | 42 ++++++++++++++ .../io/quarkus/it/keycloak/RolesService.java | 17 ++++++ .../src/main/resources/application.properties | 4 ++ .../it/keycloak/OidcTokenPropagationTest.java | 11 ++++ 26 files changed, 719 insertions(+), 4 deletions(-) create mode 100644 extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationWithSecurityIdentityAugmentorLazyAuthTest.java create mode 100644 extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationWithSecurityIdentityAugmentorTest.java create mode 100644 extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/RolesResource.java create mode 100644 extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/RolesSecurityIdentityAugmentor.java create mode 100644 extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/RolesService.java create mode 100644 extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/OidcTokenPropagationWithSecurityIdentityAugmentorLazyAuthTest.java create mode 100644 extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/OidcTokenPropagationWithSecurityIdentityAugmentorTest.java create mode 100644 extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/RolesResource.java create mode 100644 extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/RolesSecurityIdentityAugmentor.java create mode 100644 extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/RolesService.java create mode 100644 integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/RolesResource.java create mode 100644 integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/RolesSecurityIdentityAugmentor.java create mode 100644 integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/RolesService.java 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 c36e951020223b..72ff706e526579 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,23 @@ 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"); + } + + if (capabilities.isPresent(Capability.JWT)) { + return new SystemPropertyBuildItem("quarkus.smallrye-jwt.propagate-token-credential-with-duplicated-context", + "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 +79,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 47f086a3850ba3..d60ed7145b98f5 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 rely 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 c0b32fbd7bfb69..a5a3d0ee9fe97b 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 00000000000000..9cf18092144367 --- /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 00000000000000..e918f9f04d6af6 --- /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 00000000000000..80d7167434c0cf --- /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 00000000000000..47632c40fd86be --- /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 00000000000000..eed59904a431d6 --- /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 ed7c1e4981b178..28a85df55f37e9 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.enabled-during-authentication' 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.enabled-during-authentication", defaultValue = "false") + boolean enabledDuringAuthentication; 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,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 +145,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 10a38cd4ce61b2..b5f104d635939e 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,23 @@ 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"); + } + + if (capabilities.isPresent(Capability.JWT)) { + return new SystemPropertyBuildItem("quarkus.smallrye-jwt.propagate-token-credential-with-duplicated-context", + "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 +77,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 1a4ee316740976..5f0a7cd283a52d 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 00000000000000..ce2f329a4a57cc --- /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 00000000000000..f0ab3d8b7e8742 --- /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 00000000000000..2fa2ddd4404fd2 --- /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 00000000000000..03b97c253fe522 --- /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 00000000000000..5a2047e9dcff5f --- /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 6f08d7a51d887b..a87f0c74804506 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.enabled-during-authentication' 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.enabled-during-authentication", defaultValue = "false") + boolean enabledDuringAuthentication; 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(requestContext)) { + propagateToken(requestContext, exchangeTokenIfNeeded(getTokenCredentialFromContext().getToken())); + } else { + if (verifyTokenInstance(requestContext, accessToken)) { + propagateToken(requestContext, exchangeTokenIfNeeded(accessToken.get().getToken())); + } } } @@ -74,4 +85,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 84c1feda61bae6..17afda9bf02831 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 rely 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 11176370f2250a..acb1031b24ffe6 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,45 @@ 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) { + // 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 28b518ad9ff209..e3dba2d388eefa 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,6 +34,14 @@ */ @ApplicationScoped public class JWTAuthMechanism implements HttpAuthenticationMechanism { + /** + * 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.smallrye-jwt.propagate-token-credential-with-duplicated-context"); + 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"; @@ -49,6 +60,26 @@ public Uni authenticate(RoutingContext context, String jwtToken = new VertxBearerTokenExtractor(authContextInfo, context).getBearerToken(); if (jwtToken != null) { context.put(HttpAuthenticationMechanism.class.getName(), this); + + if (PROPAGATE_TOKEN_CREDENTIAL_WITH_DUPLICATED_CTX) { + // 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 ca54f476f4ab9b..fd97ad340a00d4 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 00000000000000..24a18dd122cdde --- /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 00000000000000..43c7b418bb99ae --- /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 00000000000000..6e79837657bcac --- /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 82cff223198dbc..d3396f08767f1f 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 03c22f825c57dc..fce76b77d1d78c 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"))