From 48d0c87cea8f911cb2818c2cdd67d66a5c98ee55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Wed, 14 Sep 2022 15:50:21 +0200 Subject: [PATCH] Adjust 'challenge' selection so that custom auth mechanism is called fixes: #27180 --- .../main/asciidoc/security-customization.adoc | 18 ++- .../security/CustomFormAuthChallengeTest.java | 108 ++++++++++++++++++ .../runtime/security/HttpAuthenticator.java | 19 ++- 3 files changed, 133 insertions(+), 12 deletions(-) create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomFormAuthChallengeTest.java diff --git a/docs/src/main/asciidoc/security-customization.adoc b/docs/src/main/asciidoc/security-customization.adoc index d3130db9ecb9c..456090e604ee2 100644 --- a/docs/src/main/asciidoc/security-customization.adoc +++ b/docs/src/main/asciidoc/security-customization.adoc @@ -84,6 +84,8 @@ In some cases such a default logic of selecting the challenge is exactly what is [source,java] ---- +@Alternative <1> +@Priority(1) @ApplicationScoped public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism { @@ -102,18 +104,21 @@ public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism @Override public Uni getChallenge(RoutingContext context) { - return selectBetweenJwtAndOidcChallenge(context).getChallenge(context); + return selectBetweenJwtAndOidcChallenge(context).getChallenge(context); } @Override public Set> getCredentialTypes() { - return selectBetweenJwtAndOidc(context).getCredentialTypes(); + Set> credentialTypes = new HashSet<>(); + credentialTypes.addAll(jwt.getCredentialTypes()); + credentialTypes.addAll(oidc.getCredentialTypes()); + return credentialTypes; } - @Override - public HttpCredentialTransport getCredentialTransport(RoutingContext context) { - return selectBetweenJwtAndOidc(context).getCredentialTransport(); - } + @Override + public Uni getCredentialTransport(RoutingContext context) { + return selectBetweenJwtAndOidc(context).getCredentialTransport(context); + } private HttpAuthenticationMechanism selectBetweenJwtAndOidc(RoutingContext context) { .... @@ -125,6 +130,7 @@ public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism } ---- +<1> Declaring the mechanism an alternative bean ensures this mechanism is used rather than `OidcAuthenticationMechanism` and `JWTAuthMechanism`. [[security-identity-customization]] == Security Identity Customization diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomFormAuthChallengeTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomFormAuthChallengeTest.java new file mode 100644 index 0000000000000..696e130ec95d2 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomFormAuthChallengeTest.java @@ -0,0 +1,108 @@ +package io.quarkus.vertx.http.security; + +import java.util.Set; +import java.util.function.Supplier; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Alternative; +import javax.inject.Inject; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Priority; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.quarkus.vertx.http.runtime.security.FormAuthenticationMechanism; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport; +import io.restassured.RestAssured; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +public class CustomFormAuthChallengeTest { + + private static final int EXPECTED_STATUS = 203; + private static final String EXPECTED_HEADER_NAME = "ElizabethII"; + private static final String EXPECTED_HEADER_VALUE = "CharlesIV"; + private static final String ADMIN = "admin"; + private static final String APP_PROPS = "" + + "quarkus.http.auth.form.enabled=true\n" + + "quarkus.http.auth.form.login-page=login\n" + + "quarkus.http.auth.form.error-page=error\n" + + "quarkus.http.auth.form.landing-page=landing\n" + + "quarkus.http.auth.policy.r1.roles-allowed=admin\n" + + "quarkus.http.auth.session.encryption-key=CHANGEIT-CHANGEIT-CHANGEIT-CHANGEIT-CHANGEIT\n"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(CustomFormAuthenticator.class, TestIdentityProvider.class, TestIdentityController.class, + TestTrustedIdentityProvider.class, PathHandler.class) + .addAsResource(new StringAsset(APP_PROPS), "application.properties"); + } + }); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add(ADMIN, ADMIN, ADMIN); + } + + @Test + public void testCustomGetChallengeIsCalled() { + RestAssured + .given() + .when() + .formParam("j_username", ADMIN) + .formParam("j_password", "wrong_password") + .post("/j_security_check") + .then() + .assertThat() + .statusCode(EXPECTED_STATUS) + .header(EXPECTED_HEADER_NAME, Matchers.is(EXPECTED_HEADER_VALUE)); + } + + @Alternative + @Priority(1) + @ApplicationScoped + public static class CustomFormAuthenticator implements HttpAuthenticationMechanism { + + @Inject + FormAuthenticationMechanism delegate; + + @Override + public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { + final var authenticate = delegate.authenticate(context, identityProviderManager); + context.put(HttpAuthenticationMechanism.class.getName(), this); + return authenticate; + } + + @Override + public Uni getChallenge(RoutingContext context) { + return Uni.createFrom().item(new ChallengeData(EXPECTED_STATUS, EXPECTED_HEADER_NAME, EXPECTED_HEADER_VALUE)); + } + + @Override + public Set> getCredentialTypes() { + return delegate.getCredentialTypes(); + } + + @Override + public Uni getCredentialTransport(RoutingContext context) { + return delegate.getCredentialTransport(context); + } + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java index 0a2ce19a3343d..8b2de1d915bd1 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java @@ -137,10 +137,14 @@ public Uni sendChallenge(RoutingContext routingContext) { routingContext.request().resume(); Uni result = null; - HttpAuthenticationMechanism matchingMech = routingContext.get(HttpAuthenticationMechanism.class.getName()); - if (matchingMech != null) { - result = matchingMech.sendChallenge(routingContext); + // we only require auth mechanism to put itself into routing context when there is more than one mechanism registered + if (mechanisms.length > 1) { + HttpAuthenticationMechanism matchingMech = routingContext.get(HttpAuthenticationMechanism.class.getName()); + if (matchingMech != null) { + result = matchingMech.sendChallenge(routingContext); + } } + if (result == null) { result = mechanisms[0].sendChallenge(routingContext); for (int i = 1; i < mechanisms.length; ++i) { @@ -169,9 +173,12 @@ public Uni apply(Boolean authDone) { } public Uni getChallenge(RoutingContext routingContext) { - HttpAuthenticationMechanism matchingMech = routingContext.get(HttpAuthenticationMechanism.class.getName()); - if (matchingMech != null) { - return matchingMech.getChallenge(routingContext); + // we only require auth mechanism to put itself into routing context when there is more than one mechanism registered + if (mechanisms.length > 1) { + HttpAuthenticationMechanism matchingMech = routingContext.get(HttpAuthenticationMechanism.class.getName()); + if (matchingMech != null) { + return matchingMech.getChallenge(routingContext); + } } Uni result = mechanisms[0].getChallenge(routingContext); for (int i = 1; i < mechanisms.length; ++i) {