Skip to content

Commit

Permalink
Merge pull request #34933 from michalvavrik/feature/oidc-token-propag…
Browse files Browse the repository at this point in the history
…ation-during-auth

Support OIDC token propagation  during SecurityIdentity augmentation
  • Loading branch information
sberyozkin authored Jul 27, 2023
2 parents 170ceaf + 202addd commit acff28d
Show file tree
Hide file tree
Showing 27 changed files with 770 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -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";

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -51,11 +58,35 @@ void registerProvider(BuildProducer<AdditionalBeanBuildItem> 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;

public boolean getAsBoolean() {
return config.enabled;
}
}

public static class IsEnabledDuringAuth implements BooleanSupplier {
OidcTokenPropagationReactiveBuildTimeConfig config;

public boolean getAsBoolean() {
return config.enabledDuringAuthentication;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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"));
}

}
Original file line number Diff line number Diff line change
@@ -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"));
}

}
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -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<SecurityIdentity> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<String> getRole();
}
Loading

0 comments on commit acff28d

Please sign in to comment.