diff --git a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc index bce82c0532b11..78aff7f45d014 100644 --- a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc +++ b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc @@ -600,6 +600,7 @@ The same authorization can be required with the `@PermissionsAllowed(value = { " * xref:security-authentication-mechanisms.adoc#mtls-programmatic-set-up[Set up the mutual TLS client authentication programmatically] * xref:security-cors.adoc#cors-filter-programmatic-set-up[Configuring the CORS filter programmatically] * xref:security-csrf-prevention.adoc#csrf-prevention-programmatic-set-up[Configuring the CSRF prevention programmatically] +* xref:security-jpa.adoc#programmatic-set-up[Set up Basic authentication with Jakarta Persistence programmatically] [[standard-security-annotations]] == Authorization using annotations diff --git a/docs/src/main/asciidoc/security-jpa.adoc b/docs/src/main/asciidoc/security-jpa.adoc index 4845a5c164be6..d0dd426882588 100644 --- a/docs/src/main/asciidoc/security-jpa.adoc +++ b/docs/src/main/asciidoc/security-jpa.adoc @@ -222,6 +222,31 @@ For more information about proactive authentication, see the Quarkus xref:securi include::{generated-dir}/config/quarkus-security-jpa.adoc[opts=optional, leveloffset=+2] +[[programmatic-set-up]] +== Set up Basic authentication with Jakarta Persistence programmatically + +The `io.quarkus.vertx.http.security.HttpSecurity` CDI event allows to configure the Basic authentication mechanism programmatically. +If there is more than one `IdentityProvider` handling the `io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest` request, you can configure the Basic authentication to use the Jakarta Persistence `IdentityProvider` like in the example below: + +[source,java] +---- +package org.acme.http.security; + +import static io.quarkus.security.jpa.SecurityJpa.jpa; + +import jakarta.enterprise.event.Observes; + +import io.quarkus.vertx.http.security.HttpSecurity; + +public class HttpSecurityConfiguration { + + void configure(@Observes HttpSecurity httpSecurity) { + httpSecurity.basic(jpa()); + } + +} +---- + == References * xref:security-getting-started-tutorial.adoc[Getting started with Security by using Basic authentication and Jakarta Persistence] diff --git a/extensions/security-jpa-common/runtime/src/main/java/io/quarkus/security/jpa/SecurityJpa.java b/extensions/security-jpa-common/runtime/src/main/java/io/quarkus/security/jpa/SecurityJpa.java new file mode 100644 index 0000000000000..5378aef3fb851 --- /dev/null +++ b/extensions/security-jpa-common/runtime/src/main/java/io/quarkus/security/jpa/SecurityJpa.java @@ -0,0 +1,28 @@ +package io.quarkus.security.jpa; + +import java.util.Collection; + +import io.quarkus.arc.Arc; +import io.quarkus.security.identity.IdentityProvider; + +/** + * A CDI beans used to retrieve generated Quarkus Security JPA beans. + */ +public interface SecurityJpa { + + /** + * @return Quarkus Security JPA {@link IdentityProvider}s + */ + Collection> getIdentityProviders(); + + /** + * Looks up the {@link SecurityJpa} CDI bean and returns the Quarkus Security JPA {@link IdentityProvider}s. + * + * @return Quarkus Security JPA {@link IdentityProvider}s + */ + static Collection> jpa() { + try (var securityJpaInstance = Arc.requireContainer().instance(SecurityJpa.class)) { + return securityJpaInstance.get().getIdentityProviders(); + } + } +} diff --git a/extensions/security-jpa-reactive/deployment/src/main/java/io/quarkus/security/jpa/reactive/deployment/QuarkusSecurityJpaReactiveProcessor.java b/extensions/security-jpa-reactive/deployment/src/main/java/io/quarkus/security/jpa/reactive/deployment/QuarkusSecurityJpaReactiveProcessor.java index 0ac604a2e5d27..cf46dd99b8aab 100644 --- a/extensions/security-jpa-reactive/deployment/src/main/java/io/quarkus/security/jpa/reactive/deployment/QuarkusSecurityJpaReactiveProcessor.java +++ b/extensions/security-jpa-reactive/deployment/src/main/java/io/quarkus/security/jpa/reactive/deployment/QuarkusSecurityJpaReactiveProcessor.java @@ -26,6 +26,7 @@ import org.jboss.jandex.DotName; import org.jboss.jandex.Index; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; import io.quarkus.deployment.Feature; @@ -49,6 +50,7 @@ import io.quarkus.security.jpa.common.deployment.PanacheEntityPredicateBuildItem; import io.quarkus.security.jpa.reactive.runtime.JpaReactiveIdentityProvider; import io.quarkus.security.jpa.reactive.runtime.JpaReactiveTrustedIdentityProvider; +import io.quarkus.security.jpa.reactive.runtime.SecurityJpaReactiveImpl; import io.smallrye.mutiny.Uni; class QuarkusSecurityJpaReactiveProcessor { @@ -82,6 +84,11 @@ PanacheEntityPredicateBuildItem panacheEntityPredicate(List collectPanacheEntities(List panacheEntityClassesBuildItems) { Set modelClasses = new HashSet<>(); for (PanacheEntityClassesBuildItem panacheEntityClasses : panacheEntityClassesBuildItems) { diff --git a/extensions/security-jpa-reactive/deployment/src/test/java/io/quarkus/security/jpa/reactive/SecurityJpaReactiveFluentApiTest.java b/extensions/security-jpa-reactive/deployment/src/test/java/io/quarkus/security/jpa/reactive/SecurityJpaReactiveFluentApiTest.java new file mode 100644 index 0000000000000..44a5a9b6a39f6 --- /dev/null +++ b/extensions/security-jpa-reactive/deployment/src/test/java/io/quarkus/security/jpa/reactive/SecurityJpaReactiveFluentApiTest.java @@ -0,0 +1,157 @@ +package io.quarkus.security.jpa.reactive; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; + +import java.time.Duration; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; + +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.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; +import io.quarkus.security.jpa.SecurityJpa; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.security.Form; +import io.quarkus.vertx.http.security.HttpSecurity; +import io.restassured.RestAssured; +import io.restassured.filter.cookie.CookieFilter; +import io.smallrye.mutiny.Uni; + +public class SecurityJpaReactiveFluentApiTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(() -> ShrinkWrap + .create(JavaArchive.class) + .addClasses(TestApplication.class, MinimalUserEntity.class, SecurityJpaConfiguration.class, + SingleRoleSecuredResource.class, FailingIdentityProvider.class) + .addAsResource("minimal-config/import.sql", "import.sql") + .addAsResource(new StringAsset(""" + quarkus.datasource.db-kind=postgresql + quarkus.datasource.username=${postgres.reactive.username} + quarkus.datasource.password=${postgres.reactive.password} + quarkus.datasource.reactive=true + quarkus.datasource.reactive.url=${postgres.reactive.url} + quarkus.hibernate-orm.sql-load-script=import.sql + quarkus.hibernate-orm.schema-management.strategy=drop-and-create + """), "application.properties")); + + @Test + void testFormBasedAuthentication() { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + CookieFilter cookies = new CookieFilter(); + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .get("/jaxrs-secured/user-secured") + .then() + .assertThat() + .statusCode(302) + .header("location", containsString("/login")) + .cookie("quarkus-redirect-location", containsString("/user-secured")); + + // test with a non-existent user + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .formParam("j_username", "dummy") + .formParam("j_password", "dummy") + .post("/j_security_check") + .then() + .assertThat() + .statusCode(302); + + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .formParam("j_username", "user") + .formParam("j_password", "user") + .post("/j_security_check") + .then() + .assertThat() + .statusCode(302) + .header("location", containsString("/user-secured")) + .cookie("laitnederc-sukrauq", notNullValue()); + + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .get("/jaxrs-secured/user-secured") + .then() + .assertThat() + .statusCode(200) + .body(equalTo("A secured message")); + } + + @Test + void testBasicAuthentication() { + RestAssured + .given() + .auth().preemptive().basic("user", "wrong-password") + .get("/jaxrs-secured/user-secured") + .then() + .statusCode(401); + RestAssured + .given() + .auth().preemptive().basic("user", "user") + .get("/jaxrs-secured/user-secured") + .then() + .statusCode(200) + .body(equalTo("A secured message")); + } + + public static class SecurityJpaConfiguration { + + void configure(@Observes HttpSecurity httpSecurity) { + var form = Form.builder() + .loginPage("login") + .errorPage("error") + .landingPage("landing") + .cookieName("laitnederc-sukrauq") + .newCookieInterval(Duration.ofSeconds(5)) + .timeout(Duration.ofSeconds(5)) + .encryptionKey("CHANGEIT-CHANGEIT-CHANGEIT-CHANGEIT-CHANGEIT") + .build(); + var jpa = SecurityJpa.jpa(); + httpSecurity.mechanism(form, jpa).basic(jpa); + } + + } + + @ApplicationScoped + public static class FailingIdentityProvider implements IdentityProvider { + + @Override + public Class getRequestType() { + return UsernamePasswordAuthenticationRequest.class; + } + + @Override + public Uni authenticate(UsernamePasswordAuthenticationRequest authenticationRequest, + AuthenticationRequestContext authenticationRequestContext) { + throw new IllegalStateException("This provider must never be invoked as we selected the JPA provider"); + } + + @Override + public int priority() { + return Integer.MAX_VALUE; + } + } +} diff --git a/extensions/security-jpa-reactive/runtime/src/main/java/io/quarkus/security/jpa/reactive/runtime/SecurityJpaReactiveImpl.java b/extensions/security-jpa-reactive/runtime/src/main/java/io/quarkus/security/jpa/reactive/runtime/SecurityJpaReactiveImpl.java new file mode 100644 index 0000000000000..0317a043b19f3 --- /dev/null +++ b/extensions/security-jpa-reactive/runtime/src/main/java/io/quarkus/security/jpa/reactive/runtime/SecurityJpaReactiveImpl.java @@ -0,0 +1,32 @@ +package io.quarkus.security.jpa.reactive.runtime; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.jpa.SecurityJpa; + +public final class SecurityJpaReactiveImpl implements SecurityJpa { + + @Inject + Instance jpaReactiveIdentityProvider; + + @Inject + Instance jpaReactiveTrustedIdentityProvider; + + @Override + public Collection> getIdentityProviders() { + final Collection> result = new ArrayList<>(); + if (jpaReactiveIdentityProvider.isResolvable()) { + result.add(jpaReactiveIdentityProvider.get()); + } + if (jpaReactiveTrustedIdentityProvider.isResolvable()) { + result.add(jpaReactiveTrustedIdentityProvider.get()); + } + return Collections.unmodifiableCollection(result); + } +} diff --git a/extensions/security-jpa/deployment/src/main/java/io/quarkus/security/jpa/deployment/QuarkusSecurityJpaProcessor.java b/extensions/security-jpa/deployment/src/main/java/io/quarkus/security/jpa/deployment/QuarkusSecurityJpaProcessor.java index 39960d1f3188c..bfa454698f361 100644 --- a/extensions/security-jpa/deployment/src/main/java/io/quarkus/security/jpa/deployment/QuarkusSecurityJpaProcessor.java +++ b/extensions/security-jpa/deployment/src/main/java/io/quarkus/security/jpa/deployment/QuarkusSecurityJpaProcessor.java @@ -26,6 +26,7 @@ import org.jboss.jandex.Index; import org.jboss.jandex.Type; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; import io.quarkus.arc.deployment.InjectionPointTransformerBuildItem; @@ -55,6 +56,7 @@ import io.quarkus.security.jpa.common.deployment.PanacheEntityPredicateBuildItem; import io.quarkus.security.jpa.runtime.JpaIdentityProvider; import io.quarkus.security.jpa.runtime.JpaTrustedIdentityProvider; +import io.quarkus.security.jpa.runtime.SecurityJpaImpl; class QuarkusSecurityJpaProcessor { @@ -118,6 +120,11 @@ PanacheEntityPredicateBuildItem panacheEntityPredicate(List collectPanacheEntities(List panacheEntityClassesBuildItems) { Set modelClasses = new HashSet<>(); for (PanacheEntityClassesBuildItem panacheEntityClasses : panacheEntityClassesBuildItems) { diff --git a/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/SecurityJpaFluentApiTest.java b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/SecurityJpaFluentApiTest.java new file mode 100644 index 0000000000000..3203e0373820f --- /dev/null +++ b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/SecurityJpaFluentApiTest.java @@ -0,0 +1,156 @@ +package io.quarkus.security.jpa; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; + +import java.time.Duration; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; + +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.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.security.Form; +import io.quarkus.vertx.http.security.HttpSecurity; +import io.restassured.RestAssured; +import io.restassured.filter.cookie.CookieFilter; +import io.smallrye.mutiny.Uni; + +public class SecurityJpaFluentApiTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(() -> ShrinkWrap + .create(JavaArchive.class) + .addClasses(SingleRoleSecuredServlet.class, MinimalUserEntity.class, SecurityJpaConfiguration.class, + FailingIdentityProvider.class) + .addAsResource("minimal-config/import.sql", "import.sql") + .addAsResource(new StringAsset(""" + quarkus.datasource.db-kind=h2 + quarkus.datasource.username=sa + quarkus.datasource.password=sa + quarkus.datasource.jdbc.url=jdbc:h2:mem:minimal-config + quarkus.hibernate-orm.sql-load-script=import.sql + quarkus.hibernate-orm.schema-management.strategy=drop-and-create + """), "application.properties")); + + @Test + void testFormBasedAuthentication() { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + CookieFilter cookies = new CookieFilter(); + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .get("/servlet-secured") + .then() + .assertThat() + .statusCode(302) + .header("location", containsString("/login")) + .cookie("quarkus-redirect-location", containsString("/servlet-secured")); + + // test with a non-existent user + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .formParam("j_username", "dummy") + .formParam("j_password", "dummy") + .post("/j_security_check") + .then() + .assertThat() + .statusCode(302); + + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .formParam("j_username", "user") + .formParam("j_password", "user") + .post("/j_security_check") + .then() + .assertThat() + .statusCode(302) + .header("location", containsString("/servlet-secured")) + .cookie("laitnederc-sukrauq", notNullValue()); + + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .get("/servlet-secured") + .then() + .assertThat() + .statusCode(200) + .body(equalTo("A secured message")); + } + + @Test + void testBasicAuthentication() { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + RestAssured + .given() + .auth().preemptive().basic("user", "wrong-password") + .get("/servlet-secured") + .then() + .statusCode(401); + RestAssured + .given() + .auth().preemptive().basic("user", "user") + .get("/servlet-secured") + .then() + .statusCode(200) + .body(equalTo("A secured message")); + } + + public static class SecurityJpaConfiguration { + + void configure(@Observes HttpSecurity httpSecurity) { + var form = Form.builder() + .loginPage("login") + .errorPage("error") + .landingPage("landing") + .cookieName("laitnederc-sukrauq") + .newCookieInterval(Duration.ofSeconds(5)) + .timeout(Duration.ofSeconds(5)) + .encryptionKey("CHANGEIT-CHANGEIT-CHANGEIT-CHANGEIT-CHANGEIT") + .build(); + var jpa = SecurityJpa.jpa(); + httpSecurity.mechanism(form, jpa).basic(jpa); + } + + } + + @ApplicationScoped + public static class FailingIdentityProvider implements IdentityProvider { + + @Override + public Class getRequestType() { + return UsernamePasswordAuthenticationRequest.class; + } + + @Override + public Uni authenticate(UsernamePasswordAuthenticationRequest authenticationRequest, + AuthenticationRequestContext authenticationRequestContext) { + throw new IllegalStateException("This provider must never be invoked as we selected the JPA provider"); + } + + @Override + public int priority() { + return Integer.MAX_VALUE; + } + } +} diff --git a/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/runtime/SecurityJpaImpl.java b/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/runtime/SecurityJpaImpl.java new file mode 100644 index 0000000000000..b31ec0fafab35 --- /dev/null +++ b/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/runtime/SecurityJpaImpl.java @@ -0,0 +1,33 @@ +package io.quarkus.security.jpa.runtime; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.jpa.SecurityJpa; + +public final class SecurityJpaImpl implements SecurityJpa { + + @Inject + Instance jpaIdentityProvider; + + @Inject + Instance jpaTrustedIdentityProvider; + + @Override + public Collection> getIdentityProviders() { + final Collection> result = new ArrayList<>(); + if (jpaIdentityProvider.isResolvable()) { + result.add(jpaIdentityProvider.get()); + } + if (jpaTrustedIdentityProvider.isResolvable()) { + result.add(jpaTrustedIdentityProvider.get()); + } + return Collections.unmodifiableCollection(result); + } + +} diff --git a/extensions/security/deployment/pom.xml b/extensions/security/deployment/pom.xml index 14a37927038ef..9758397242872 100644 --- a/extensions/security/deployment/pom.xml +++ b/extensions/security/deployment/pom.xml @@ -50,6 +50,11 @@ bcprov-jdk18on test + + org.assertj + assertj-core + test + diff --git a/extensions/security/deployment/src/test/java/io/quarkus/security/test/cdi/IdentityProviderManagerBuilderTest.java b/extensions/security/deployment/src/test/java/io/quarkus/security/test/cdi/IdentityProviderManagerBuilderTest.java new file mode 100644 index 0000000000000..3499be98b112b --- /dev/null +++ b/extensions/security/deployment/src/test/java/io/quarkus/security/test/cdi/IdentityProviderManagerBuilderTest.java @@ -0,0 +1,248 @@ +package io.quarkus.security.test.cdi; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collection; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; +import io.quarkus.security.identity.request.AnonymousAuthenticationRequest; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.identity.request.BaseAuthenticationRequest; +import io.quarkus.security.runtime.QuarkusPrincipal; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.quarkus.security.spi.runtime.IdentityProviderManagerBuilder; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.mutiny.Uni; + +public class IdentityProviderManagerBuilderTest { + + private static final String GLOBAL_AUGMENTOR_1 = "global-augmentor-1"; + private static final String GLOBAL_AUGMENTOR_2 = "global-augmentor-2"; + private static final String LOCAL_AUGMENTOR_1 = "local-augmentor-1"; + private static final String LOCAL_AUGMENTOR_2 = "local-augmentor-2"; + private static final String GLOBAL_IDENTITY_PROVIDER_1 = "global-identity-provider-1"; + private static final String GLOBAL_IDENTITY_PROVIDER_2 = "global-identity-provider-2"; + private static final String LOCAL_IDENTITY_PROVIDER_1 = "local-identity-provider-1"; + private static final String LOCAL_IDENTITY_PROVIDER_2 = "local-identity-provider-2"; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(TestProducer.class, NamedIdentityAugmentor.class, TestAuthRequest.class, + TestIdentityProvider.class, TestAuthRequest2.class)); + + @Inject + IdentityProviderManagerBuilder builder; + + @Test + void noLocalProviderOrAugmentor() { + // verify only global augmentors and identity providers are applied + IdentityProviderManager ipm = builder.build(null, null); + SecurityIdentity securityIdentity = ipm.authenticateBlocking(new TestAuthRequest()); + assertThat(securityIdentity.getAttributes()) + .doesNotContainValue(LOCAL_AUGMENTOR_1) + .doesNotContainValue(LOCAL_AUGMENTOR_2) + .doesNotContainValue(LOCAL_IDENTITY_PROVIDER_2) + .containsValues(GLOBAL_IDENTITY_PROVIDER_1, GLOBAL_AUGMENTOR_1, GLOBAL_AUGMENTOR_2); + securityIdentity = ipm.authenticateBlocking(new TestAuthRequest2()); + assertThat(securityIdentity.getAttributes()) + .doesNotContainValue(LOCAL_AUGMENTOR_1) + .doesNotContainValue(LOCAL_AUGMENTOR_2) + .doesNotContainValue(LOCAL_IDENTITY_PROVIDER_1) + .containsValues(GLOBAL_IDENTITY_PROVIDER_2, GLOBAL_AUGMENTOR_1, GLOBAL_AUGMENTOR_2); + securityIdentity = ipm.authenticateBlocking(new AnonymousAuthenticationRequest()); + assertThat(securityIdentity.getAttributes()) + .doesNotContainValue(LOCAL_AUGMENTOR_1) + .doesNotContainValue(LOCAL_AUGMENTOR_2) + .doesNotContainValue(LOCAL_IDENTITY_PROVIDER_1) + .doesNotContainValue(LOCAL_IDENTITY_PROVIDER_2) + .doesNotContainValue(GLOBAL_IDENTITY_PROVIDER_1) + .doesNotContainValue(GLOBAL_IDENTITY_PROVIDER_2) + .containsValues(GLOBAL_AUGMENTOR_1, GLOBAL_AUGMENTOR_2); + } + + @Test + void onlyLocalProvidersAndAugmentors() { + Collection> providers = List.of( + new TestIdentityProvider<>(LOCAL_IDENTITY_PROVIDER_1, TestAuthRequest.class), + new TestIdentityProvider<>(LOCAL_IDENTITY_PROVIDER_2, TestAuthRequest2.class)); + Collection augmentors = List.of( + new NamedIdentityAugmentor(LOCAL_AUGMENTOR_1), + new NamedIdentityAugmentor(LOCAL_AUGMENTOR_2)); + IdentityProviderManager ipm = builder.build(providers, augmentors); + SecurityIdentity securityIdentity = ipm.authenticateBlocking(new TestAuthRequest()); + assertThat(securityIdentity.getAttributes()) + .doesNotContainValue(GLOBAL_AUGMENTOR_1) + .doesNotContainValue(GLOBAL_AUGMENTOR_2) + .doesNotContainValue(LOCAL_IDENTITY_PROVIDER_2) + .doesNotContainValue(GLOBAL_IDENTITY_PROVIDER_1) + .doesNotContainValue(GLOBAL_IDENTITY_PROVIDER_2) + .containsValues(LOCAL_IDENTITY_PROVIDER_1, LOCAL_AUGMENTOR_1, LOCAL_AUGMENTOR_2); + securityIdentity = ipm.authenticateBlocking(new TestAuthRequest2()); + assertThat(securityIdentity.getAttributes()) + .doesNotContainValue(GLOBAL_AUGMENTOR_1) + .doesNotContainValue(GLOBAL_AUGMENTOR_2) + .doesNotContainValue(LOCAL_IDENTITY_PROVIDER_1) + .doesNotContainValue(GLOBAL_IDENTITY_PROVIDER_1) + .doesNotContainValue(GLOBAL_IDENTITY_PROVIDER_2) + .containsValues(LOCAL_IDENTITY_PROVIDER_2, LOCAL_AUGMENTOR_1, LOCAL_AUGMENTOR_2); + securityIdentity = ipm.authenticateBlocking(new AnonymousAuthenticationRequest()); + assertThat(securityIdentity.getAttributes()) + .doesNotContainValue(GLOBAL_AUGMENTOR_1) + .doesNotContainValue(GLOBAL_AUGMENTOR_2) + .doesNotContainValue(LOCAL_IDENTITY_PROVIDER_1) + .doesNotContainValue(LOCAL_IDENTITY_PROVIDER_2) + .doesNotContainValue(GLOBAL_IDENTITY_PROVIDER_1) + .doesNotContainValue(GLOBAL_IDENTITY_PROVIDER_2) + .containsValues(LOCAL_AUGMENTOR_1, LOCAL_AUGMENTOR_2); + } + + @Test + void localProviderAndGlobalAugmentors() { + Collection> providers = List.of( + new TestIdentityProvider<>(LOCAL_IDENTITY_PROVIDER_1, TestAuthRequest.class)); + IdentityProviderManager ipm = builder.build(providers, null); + SecurityIdentity securityIdentity = ipm.authenticateBlocking(new TestAuthRequest()); + assertThat(securityIdentity.getAttributes()) + .doesNotContainValue(LOCAL_AUGMENTOR_1) + .doesNotContainValue(LOCAL_AUGMENTOR_2) + .doesNotContainValue(LOCAL_IDENTITY_PROVIDER_2) + .doesNotContainValue(GLOBAL_IDENTITY_PROVIDER_1) + .doesNotContainValue(GLOBAL_IDENTITY_PROVIDER_2) + .containsValues(LOCAL_IDENTITY_PROVIDER_1, GLOBAL_AUGMENTOR_1, GLOBAL_AUGMENTOR_2); + securityIdentity = ipm.authenticateBlocking(new AnonymousAuthenticationRequest()); + assertThat(securityIdentity.getAttributes()) + .doesNotContainValue(LOCAL_AUGMENTOR_1) + .doesNotContainValue(LOCAL_AUGMENTOR_2) + .doesNotContainValue(LOCAL_IDENTITY_PROVIDER_1) + .doesNotContainValue(LOCAL_IDENTITY_PROVIDER_2) + .doesNotContainValue(GLOBAL_IDENTITY_PROVIDER_1) + .doesNotContainValue(GLOBAL_IDENTITY_PROVIDER_2) + .containsValues(GLOBAL_AUGMENTOR_1, GLOBAL_AUGMENTOR_2); + } + + @Test + void globalProvidersAndLocalAugmentor() { + Collection augmentors = List.of( + new NamedIdentityAugmentor(LOCAL_AUGMENTOR_1)); + IdentityProviderManager ipm = builder.build(null, augmentors); + SecurityIdentity securityIdentity = ipm.authenticateBlocking(new TestAuthRequest()); + assertThat(securityIdentity.getAttributes()) + .doesNotContainValue(GLOBAL_AUGMENTOR_1) + .doesNotContainValue(GLOBAL_AUGMENTOR_2) + .doesNotContainValue(LOCAL_AUGMENTOR_2) + .doesNotContainValue(LOCAL_IDENTITY_PROVIDER_2) + .doesNotContainValue(LOCAL_IDENTITY_PROVIDER_1) + .doesNotContainValue(GLOBAL_IDENTITY_PROVIDER_2) + .containsValues(GLOBAL_IDENTITY_PROVIDER_1, LOCAL_AUGMENTOR_1); + securityIdentity = ipm.authenticateBlocking(new TestAuthRequest2()); + assertThat(securityIdentity.getAttributes()) + .doesNotContainValue(GLOBAL_AUGMENTOR_1) + .doesNotContainValue(GLOBAL_AUGMENTOR_2) + .doesNotContainValue(LOCAL_AUGMENTOR_2) + .doesNotContainValue(LOCAL_IDENTITY_PROVIDER_2) + .doesNotContainValue(LOCAL_IDENTITY_PROVIDER_1) + .doesNotContainValue(GLOBAL_IDENTITY_PROVIDER_1) + .containsValues(GLOBAL_IDENTITY_PROVIDER_2, LOCAL_AUGMENTOR_1); + securityIdentity = ipm.authenticateBlocking(new AnonymousAuthenticationRequest()); + assertThat(securityIdentity.getAttributes()) + .doesNotContainValue(GLOBAL_AUGMENTOR_1) + .doesNotContainValue(GLOBAL_AUGMENTOR_2) + .doesNotContainValue(LOCAL_IDENTITY_PROVIDER_1) + .doesNotContainValue(LOCAL_IDENTITY_PROVIDER_2) + .doesNotContainValue(GLOBAL_IDENTITY_PROVIDER_1) + .doesNotContainValue(GLOBAL_IDENTITY_PROVIDER_2) + .doesNotContainValue(LOCAL_AUGMENTOR_2) + .containsValues(LOCAL_AUGMENTOR_1); + } + + public static class TestProducer { + + @Produces + @ApplicationScoped + SecurityIdentityAugmentor getGlobalAugmentor1() { + return new NamedIdentityAugmentor(GLOBAL_AUGMENTOR_1); + } + + @Produces + @ApplicationScoped + SecurityIdentityAugmentor getGlobalAugmentor2() { + return new NamedIdentityAugmentor(GLOBAL_AUGMENTOR_2); + } + + @Produces + @ApplicationScoped + IdentityProvider getGlobalIdentityProvider1() { + return new TestIdentityProvider<>(GLOBAL_IDENTITY_PROVIDER_1, TestAuthRequest.class); + } + + @Produces + @ApplicationScoped + IdentityProvider getGlobalIdentityProvider2() { + return new TestIdentityProvider<>(GLOBAL_IDENTITY_PROVIDER_2, TestAuthRequest2.class); + } + + } + + private static final class NamedIdentityAugmentor implements SecurityIdentityAugmentor { + private final String name; + + private NamedIdentityAugmentor(String name) { + this.name = name; + } + + @Override + public Uni augment(SecurityIdentity securityIdentity, + AuthenticationRequestContext authenticationRequestContext) { + return Uni.createFrom().item(QuarkusSecurityIdentity.builder(securityIdentity) + .addAttribute(name, name) + .build()); + } + } + + private static final class TestAuthRequest extends BaseAuthenticationRequest { + + } + + private static final class TestAuthRequest2 extends BaseAuthenticationRequest { + + } + + private static final class TestIdentityProvider implements IdentityProvider { + + private final String name; + private final Class requestClass; + + private TestIdentityProvider(String name, Class requestClass) { + this.name = name; + this.requestClass = requestClass; + } + + @Override + public Class getRequestType() { + return requestClass; + } + + @Override + public Uni authenticate(T testAuthRequest, + AuthenticationRequestContext authenticationRequestContext) { + return Uni.createFrom().item(QuarkusSecurityIdentity.builder() + .setAnonymous(false) + .setPrincipal(new QuarkusPrincipal("test")) + .addAttribute(name, name) + .build()); + } + } +} diff --git a/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/IdentityProviderManagerBuilder.java b/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/IdentityProviderManagerBuilder.java new file mode 100644 index 0000000000000..4875c0d0af8dd --- /dev/null +++ b/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/IdentityProviderManagerBuilder.java @@ -0,0 +1,27 @@ +package io.quarkus.security.spi.runtime; + +import java.util.Collection; + +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentityAugmentor; + +/** + * CDI bean that enables Quarkus core extensions to create own {@link IdentityProviderManager} + * from the global {@link IdentityProviderManager} used by Quarkus. + */ +public interface IdentityProviderManagerBuilder { + + /** + * Create a new {@link IdentityProviderManager}. + * + * @param identityProviders {@link IdentityProvider}s; if specified, globally configured {@link IdentityProvider}s + * will be ignored + * @param securityIdentityAugmentors {@link SecurityIdentityAugmentor}s; if specified, globally configured + * {@link SecurityIdentityAugmentor} will be ignored + * @return the new {@link IdentityProviderManager} + */ + IdentityProviderManager build(Collection> identityProviders, + Collection securityIdentityAugmentors); + +} diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/IdentityProviderManagerCreator.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/IdentityProviderManagerCreator.java index dd27e39bd7df5..751bca534158a 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/IdentityProviderManagerCreator.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/IdentityProviderManagerCreator.java @@ -1,5 +1,6 @@ package io.quarkus.security.runtime; +import java.util.List; import java.util.concurrent.Executor; import java.util.function.Supplier; @@ -14,6 +15,7 @@ import io.quarkus.security.identity.SecurityIdentityAugmentor; import io.quarkus.security.identity.request.AnonymousAuthenticationRequest; import io.quarkus.security.spi.runtime.BlockingSecurityExecutor; +import io.quarkus.security.spi.runtime.IdentityProviderManagerBuilder; /** * CDI bean than manages the lifecycle of the {@link io.quarkus.security.identity.IdentityProviderManager} @@ -34,8 +36,39 @@ public Executor get() { @Produces @ApplicationScoped - public IdentityProviderManager ipm(Instance> identityProviders, + IdentityProviderManager ipm(Instance> identityProviders, Instance augmentors, BlockingSecurityExecutor blockingExecutor) { + return createIdentityProviderManager(identityProviders, augmentors, blockingExecutor, null); + } + + @Produces + @ApplicationScoped + IdentityProviderManagerBuilder identityProviderManagerBuilder(Instance> globalIdentityProviders, + Instance globalAugmentors, + BlockingSecurityExecutor blockingExecutor) { + return (localIdentityProviders, localIdentityAugmentors) -> { + final Iterable> identityProviders; + final Iterable augmentors; + final Iterable additionalAugmentors; + if (localIdentityProviders == null || localIdentityProviders.isEmpty()) { + identityProviders = globalIdentityProviders; + } else { + identityProviders = localIdentityProviders; + } + if (localIdentityAugmentors == null || localIdentityAugmentors.isEmpty()) { + augmentors = globalAugmentors; + additionalAugmentors = null; + } else { + augmentors = localIdentityAugmentors; + additionalAugmentors = keepOnlyBuiltinAugmentors(globalAugmentors); + } + return createIdentityProviderManager(identityProviders, augmentors, blockingExecutor, additionalAugmentors); + }; + } + + private static QuarkusIdentityProviderManagerImpl createIdentityProviderManager( + Iterable> identityProviders, Iterable augmentors, + BlockingSecurityExecutor blockingExecutor, Iterable additionalAugmentors) { boolean customAnon = false; QuarkusIdentityProviderManagerImpl.Builder builder = QuarkusIdentityProviderManagerImpl.builder(); for (var i : identityProviders) { @@ -50,8 +83,23 @@ public IdentityProviderManager ipm(Instance> identityProvide for (SecurityIdentityAugmentor i : augmentors) { builder.addSecurityIdentityAugmentor(i); } + if (additionalAugmentors != null) { + for (SecurityIdentityAugmentor additionalAugmentor : additionalAugmentors) { + builder.addSecurityIdentityAugmentor(additionalAugmentor); + } + } builder.setBlockingExecutor(blockingExecutor); return builder.build(); } + private static Iterable keepOnlyBuiltinAugmentors( + Iterable augmentors) { + for (SecurityIdentityAugmentor augmentor : augmentors) { + if (augmentor instanceof QuarkusPermissionSecurityIdentityAugmentor) { + // allows @PermissionCheckers grant permissions to identity + return List.of(augmentor); + } + } + return null; + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityConfiguration.java index e1a1b904039b0..9a7f9b0564314 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityConfiguration.java @@ -3,6 +3,8 @@ import static io.quarkus.vertx.http.runtime.options.HttpServerTlsConfig.getTlsClientAuth; import static io.quarkus.vertx.http.runtime.security.HttpAuthenticator.BASIC_AUTH_ANNOTATION_DETECTED; import static io.quarkus.vertx.http.runtime.security.HttpAuthenticator.TEST_IF_BASIC_AUTH_IMPLICITLY_REQUIRED; +import static io.quarkus.vertx.http.runtime.security.HttpSecurityImpl.getMechanismClass; +import static io.quarkus.vertx.http.runtime.security.HttpSecurityImpl.unwrapMechanism; import java.util.ArrayList; import java.util.Comparator; @@ -120,8 +122,8 @@ MtlsAuthenticationMechanism getMtlsAuthenticationMechanism() { return null; } for (HttpAuthenticationMechanism additionalMechanism : additionalMechanisms) { - if (additionalMechanism.getClass() == MtlsAuthenticationMechanism.class) { - return (MtlsAuthenticationMechanism) additionalMechanism; + if (getMechanismClass(additionalMechanism) == MtlsAuthenticationMechanism.class) { + return (MtlsAuthenticationMechanism) unwrapMechanism(additionalMechanism); } } var mTLS = Arc.container().select(MtlsAuthenticationMechanism.class).orNull(); @@ -137,10 +139,11 @@ HttpAuthenticationMechanism[] getMechanisms(Instance> provid Instance mechanismsFromCdi = Arc.container().select(HttpAuthenticationMechanism.class); final HttpAuthenticationMechanism[] result; List mechanisms = new ArrayList<>(); - for (HttpAuthenticationMechanism mechanism : mechanismsFromCdi) { + for (HttpAuthenticationMechanism mechanism : additionalMechanisms) { + // prioritize programmatically configured mechanisms over CDI beans addAuthenticationMechanism(providers, mechanism, mechanisms); } - for (HttpAuthenticationMechanism mechanism : additionalMechanisms) { + for (HttpAuthenticationMechanism mechanism : mechanismsFromCdi) { addAuthenticationMechanism(providers, mechanism, mechanisms); } addBasicAuthMechanismIfImplicitlyRequired(mechanismsFromCdi, mechanisms, providers); @@ -159,14 +162,14 @@ public int compare(HttpAuthenticationMechanism mech1, HttpAuthenticationMechanis // if inclusive auth and mTLS are enabled, the mTLS must have the highest priority if (inclusiveAuth && getMtlsAuthenticationMechanism() != null) { var topMechanism = ClientProxy.unwrap(result[0]); - boolean isMutualTls = topMechanism instanceof MtlsAuthenticationMechanism; + boolean isMutualTls = unwrapMechanism(topMechanism) instanceof MtlsAuthenticationMechanism; if (!isMutualTls) { throw new IllegalStateException( """ Inclusive authentication is enabled and '%s' does not have the highest priority. Please lower priority of the '%s' authentication mechanism under '%s'. """.formatted(MtlsAuthenticationMechanism.class.getName(), - topMechanism.getClass().getName(), + getMechanismClass(topMechanism).getName(), MtlsAuthenticationMechanism.INCLUSIVE_AUTHENTICATION_PRIORITY)); } } @@ -213,7 +216,7 @@ private static synchronized HttpSecurityConfiguration initializeHttpSecurityConf if (basicAuthEnabled.isEmpty() || !basicAuthEnabled.get()) { for (HttpAuthenticationMechanism mechanism : mechanisms) { // not using instance of as we are not considering subclasses - if (mechanism.getClass() == BasicAuthenticationMechanism.class) { + if (getMechanismClass(mechanism) == BasicAuthenticationMechanism.class) { basicAuthEnabled = Optional.of(Boolean.TRUE); break; } @@ -225,9 +228,10 @@ private static synchronized HttpSecurityConfiguration initializeHttpSecurityConf if (!formAuthEnabled) { for (HttpAuthenticationMechanism mechanism : mechanisms) { // not using instance of as we are not considering subclasses - if (mechanism.getClass() == FormAuthenticationMechanism.class) { + if (getMechanismClass(mechanism) == FormAuthenticationMechanism.class) { formAuthEnabled = true; - formPostLocation = ((FormAuthenticationMechanism) mechanism).getPostLocation(); + var formMechanism = (FormAuthenticationMechanism) unwrapMechanism(mechanism); + formPostLocation = formMechanism.getPostLocation(); break; } } @@ -327,10 +331,20 @@ public PolicyMappingConfig.AppliesTo getAppliesTo() { private void addAuthenticationMechanism(Instance> providers, HttpAuthenticationMechanism mechanism, List mechanisms) { + if (isBuiltinMechanism(mechanism)) { + var clazz = getMechanismClass(mechanism); + for (var m : mechanisms) { + if (getMechanismClass(m) == clazz) { + return; + } + } + } + if (mechanism.getCredentialTypes().isEmpty()) { // mechanism does not require any IdentityProvider LOG.debugf("HttpAuthenticationMechanism '%s' provided no required credential types, therefore it needs " - + "to be able to perform authentication without any IdentityProvider", mechanism.getClass().getName()); + + "to be able to perform authentication without any IdentityProvider", + getMechanismClass(mechanism).getName()); mechanisms.add(mechanism); return; } @@ -350,7 +364,7 @@ private void addAuthenticationMechanism(Instance> providers, } if (found) { mechanisms.add(mechanism); - } else if (BasicAuthenticationMechanism.class.equals(mechanism.getClass()) && basicAuthEnabled.isEmpty()) { + } else if (BasicAuthenticationMechanism.class.equals(getMechanismClass(mechanism)) && basicAuthEnabled.isEmpty()) { LOG.debug(""" BasicAuthenticationMechanism has been enabled because no other authentication mechanism has been detected, but there is no IdentityProvider based on username and password. Please use @@ -362,7 +376,7 @@ private void addAuthenticationMechanism(Instance> providers, HttpAuthenticationMechanism '%s' requires one or more IdentityProviders supporting at least one of the following credentials types: %s. Please refer to the https://quarkus.io/guides/security-identity-providers for more information. - """.formatted(mechanism.getClass().getName(), mechanism.getCredentialTypes())); + """.formatted(getMechanismClass(mechanism).getName(), mechanism.getCredentialTypes())); } } @@ -471,6 +485,17 @@ private static boolean isHttpSecurityEventNotObserved(ArcContainer container) { return container.beanManager().resolveObserverMethods(new HttpSecurityImpl(null, null, Optional.empty())).isEmpty(); } + private static boolean isBuiltinMechanism(HttpAuthenticationMechanism actualMechanism) { + // whether this is one of 3 mechanisms we allow to configure programmatically, but we also may need + // for backward compatibility as CDI beans; for example, basic authentication mechanism enabled when no other + // mechanism recognizable during the build time was present is a CDI bean, but users may still configure + // the basic authentication during the runtime programmatically, which must have a priority; + // following 3 beans are singletons, and we only care for exact matches, not subclasses + var clazz = getMechanismClass(actualMechanism); + return clazz == BasicAuthenticationMechanism.class || clazz == MtlsAuthenticationMechanism.class + || clazz == FormAuthenticationMechanism.class; + } + public static final class ProgrammaticTlsConfig { public final ClientAuth tlsClientAuth; public final Optional tlsConfigName; diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityImpl.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityImpl.java index 4c1953e4eb918..22bf4260623d8 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityImpl.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityImpl.java @@ -3,6 +3,7 @@ import java.security.Permission; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; @@ -18,7 +19,12 @@ import io.quarkus.arc.Arc; import io.quarkus.security.StringPermission; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.spi.runtime.IdentityProviderManagerBuilder; import io.quarkus.tls.TlsConfiguration; import io.quarkus.tls.TlsConfigurationRegistry; import io.quarkus.vertx.http.runtime.FormAuthConfig; @@ -116,20 +122,21 @@ public HttpSecurity csrf(CSRF csrf) { @Override public HttpSecurity mechanism(HttpAuthenticationMechanism mechanism) { Objects.requireNonNull(mechanism); - if (mechanism.getClass() == FormAuthenticationMechanism.class) { + final HttpAuthenticationMechanism validatedMechanism = unwrapMechanism(mechanism); + if (validatedMechanism.getClass() == FormAuthenticationMechanism.class) { final FormAuthConfig defaults = HttpSecurityUtils.getDefaultAuthConfig().auth().form(); final FormAuthConfig actualConfig = vertxHttpConfig.auth().form(); if (!actualConfig.equals(defaults)) { throw new IllegalArgumentException("Cannot configure form-based authentication programmatically " + "because it has already been configured in the 'application.properties' file"); } - } else if (mechanism.getClass() == BasicAuthenticationMechanism.class) { + } else if (validatedMechanism.getClass() == BasicAuthenticationMechanism.class) { String actualRealm = vertxHttpConfig.auth().realm().orElse(null); if (actualRealm != null) { throw new IllegalArgumentException("Cannot configure basic authentication programmatically because " + "the authentication realm has already been configured in the 'application.properties' file"); } - } else if (mechanism.getClass() == MtlsAuthenticationMechanism.class) { + } else if (validatedMechanism.getClass() == MtlsAuthenticationMechanism.class) { boolean mTlsEnabled = !ClientAuth.NONE.equals(clientAuth); if (mTlsEnabled) { // current we do not allow "merging" (or overriding) of the configuration provided in application.properties @@ -139,7 +146,7 @@ public HttpSecurity mechanism(HttpAuthenticationMechanism mechanism) { throw new IllegalArgumentException("TLS client authentication has already been enabled with this API or" + " with the 'quarkus.http.ssl.client-auth' configuration property"); } - var mTLS = ((MtlsAuthenticationMechanism) mechanism); + var mTLS = (MtlsAuthenticationMechanism) validatedMechanism; clientAuth = mTLS.getTlsClientAuth(); if (mTLS.getHttpServerTlsConfigName().isPresent()) { if (httpServerTlsConfigName.isPresent()) { @@ -163,6 +170,27 @@ public HttpSecurity mechanism(HttpAuthenticationMechanism mechanism) { return this; } + @Override + public HttpSecurity mechanism(HttpAuthenticationMechanism mechanism, Collection> identityProviders, + SecurityIdentityAugmentor... identityAugmentors) { + final Collection securityIdentityAugmentors; + if (identityAugmentors == null || identityAugmentors.length == 0) { + if (identityProviders == null || identityProviders.isEmpty()) { + return mechanism(mechanism); + } + securityIdentityAugmentors = null; + } else { + securityIdentityAugmentors = List.of(identityAugmentors); + } + final Collection> identityProvidersList; + if (identityProviders == null || identityProviders.isEmpty()) { + identityProvidersList = null; + } else { + identityProvidersList = List.copyOf(identityProviders); + } + return mechanism(createMechanism(mechanism, identityProvidersList, securityIdentityAugmentors)); + } + @Override public HttpSecurity basic() { return mechanism(Basic.create()); @@ -173,6 +201,11 @@ public HttpSecurity basic(String authenticationRealm) { return mechanism(Basic.realm(authenticationRealm)); } + @Override + public HttpSecurity basic(Collection> identityProviders) { + return mechanism(Basic.create(), identityProviders); + } + @Override public HttpSecurity mTLS() { return mTLS(ClientAuth.REQUIRED); @@ -644,4 +677,58 @@ CORSConfig getCorsConfig() { CSRF getCsrf() { return csrf; } + + private static HttpAuthenticationMechanism createMechanism(HttpAuthenticationMechanism mechanism, + Collection> identityProviders, Collection identityAugmentors) { + if (identityProviders == null && identityAugmentors == null) { + return mechanism; + } + IdentityProviderManager customIdentityProviderManager = Arc.container().instance(IdentityProviderManagerBuilder.class) + .get().build(identityProviders, identityAugmentors); + return new DelegatingHttpAuthenticationMechanism(mechanism, customIdentityProviderManager); + } + + private record DelegatingHttpAuthenticationMechanism(HttpAuthenticationMechanism delegate, + IdentityProviderManager customIdentityProviderManager) implements HttpAuthenticationMechanism { + @Override + public Uni authenticate(RoutingContext context, IdentityProviderManager ignored) { + return delegate.authenticate(context, customIdentityProviderManager); + } + + @Override + public Uni getChallenge(RoutingContext context) { + return delegate.getChallenge(context); + } + + @Override + public Set> getCredentialTypes() { + return delegate.getCredentialTypes(); + } + + @Override + public Uni sendChallenge(RoutingContext context) { + return delegate.sendChallenge(context); + } + + @Override + public Uni getCredentialTransport(RoutingContext context) { + return delegate.getCredentialTransport(context); + } + + @Override + public int getPriority() { + return delegate.getPriority(); + } + } + + static Class getMechanismClass(HttpAuthenticationMechanism mechanism) { + return unwrapMechanism(mechanism).getClass(); + } + + static HttpAuthenticationMechanism unwrapMechanism(HttpAuthenticationMechanism mechanism) { + if (mechanism instanceof DelegatingHttpAuthenticationMechanism delegatingMechanism) { + return delegatingMechanism.delegate(); + } + return mechanism; + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/security/HttpSecurity.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/security/HttpSecurity.java index fc1c2688696d2..28d3c6f5af7f9 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/security/HttpSecurity.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/security/HttpSecurity.java @@ -1,13 +1,16 @@ package io.quarkus.vertx.http.security; import java.security.Permission; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.BiPredicate; import java.util.function.Predicate; +import io.quarkus.security.identity.IdentityProvider; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; import io.quarkus.tls.TlsConfiguration; import io.quarkus.vertx.http.runtime.VertxHttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.cors.CORSConfig; @@ -127,6 +130,19 @@ public interface HttpSecurity { */ HttpSecurity mechanism(HttpAuthenticationMechanism mechanism); + /** + * Registers given {@link HttpAuthenticationMechanism} in addition to all other global authentication mechanisms. + * + * @param mechanism {@link HttpAuthenticationMechanism} + * @param identityProviders {@link IdentityProvider}s that should be used for authentication instead + * {@link IdentityProvider}s registered as CDI beans; if this parameter is null, the global providers are used + * @param identityAugmentors {@link SecurityIdentityAugmentor}s that should be used to augment {@link SecurityIdentity} + * instead of the {@link SecurityIdentityAugmentor}s registered as CDI beans; this parameter is optional + * @return HttpSecurity + */ + HttpSecurity mechanism(HttpAuthenticationMechanism mechanism, Collection> identityProviders, + SecurityIdentityAugmentor... identityAugmentors); + /** * Registers the Basic authentication mechanism in addition to all other global authentication mechanisms. * This method is a shortcut for {@code mechanism(Basic.create())}. @@ -144,6 +160,16 @@ public interface HttpSecurity { */ HttpSecurity basic(String authenticationRealm); + /** + * Registers the Basic authentication mechanism in addition to all other global authentication mechanisms. + * This method is a shortcut for {@code mechanism(Basic.create(), identityProviders)}. + * + * @param identityProviders such as the Quarkus Security JPA {@link IdentityProvider}s + * @return HttpSecurity + * @see #mechanism(HttpAuthenticationMechanism, Collection, SecurityIdentityAugmentor...) + */ + HttpSecurity basic(Collection> identityProviders); + /** * Registers the mutual TLS client authentication mechanism in addition to all other global authentication mechanisms. * This method is a shortcut for {@code mTLS(ClientAuth.REQUIRED)}, therefore the client authentication is required.