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 b1f382e7bd154..6af1743a33d40 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 Security JPA 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..3f75720ade455 100644 --- a/docs/src/main/asciidoc/security-jpa.adoc +++ b/docs/src/main/asciidoc/security-jpa.adoc @@ -222,6 +222,36 @@ 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 Security JPA programmatically + +If you need to combine authentication mechanisms with different identity providers or persistence units, you can leverage the `io.quarkus.vertx.http.security.HttpSecurity` CDI event. +For example, if you combine the built-in Basic and the Form-based authentication mechanisms, you can configure different persistence unit for each of mechanism like in the example below: + +[source,java] +---- +package org.acme.http.security; + +import static io.quarkus.security.jpa.SecurityJpa.jpa; +import static io.quarkus.security.jpa.SecurityJpa.jpaTrustedProvider; + +import io.quarkus.vertx.http.security.Form; +import io.quarkus.vertx.http.security.HttpSecurity; +import jakarta.enterprise.event.Observes; + +class HttpSecurityConfiguration { + + void configure(@Observes HttpSecurity httpSecurity) { + httpSecurity + .basic(jpa("basic-pu")) <1> + .mechanism(Form.builder().identityProviders(jpa("form-pu"), jpaTrustedProvider("form-pu")).build()); <2> + } + +} +---- +<1> Configure the Basic authentication to store the users information in the `basic-pu` persistence unit. +<2> Also use the Security JPA identity providers, but this time with the `form-pu` persistence unit. + == 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/deployment/src/main/java/io/quarkus/security/jpa/common/deployment/QuarkusSecurityJpaCommonProcessor.java b/extensions/security-jpa-common/deployment/src/main/java/io/quarkus/security/jpa/common/deployment/QuarkusSecurityJpaCommonProcessor.java index 264e6eaa934ec..f22ce304fb6ab 100644 --- a/extensions/security-jpa-common/deployment/src/main/java/io/quarkus/security/jpa/common/deployment/QuarkusSecurityJpaCommonProcessor.java +++ b/extensions/security-jpa-common/deployment/src/main/java/io/quarkus/security/jpa/common/deployment/QuarkusSecurityJpaCommonProcessor.java @@ -1,21 +1,29 @@ package io.quarkus.security.jpa.common.deployment; import static io.quarkus.security.jpa.common.deployment.JpaSecurityIdentityUtil.getSingleAnnotatedElement; +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; +import static org.objectweb.asm.Opcodes.ACC_STATIC; import java.util.List; +import java.util.Optional; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.ApplicationIndexBuildItem; +import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem; +import io.quarkus.gizmo.ClassTransformer; +import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.security.jpa.Password; import io.quarkus.security.jpa.Roles; import io.quarkus.security.jpa.UserDefinition; import io.quarkus.security.jpa.Username; +import io.quarkus.security.jpa.common.runtime.JpaIdentityProviderUtil; class QuarkusSecurityJpaCommonProcessor { @@ -48,4 +56,50 @@ void provideJpaSecurityDefinition(ApplicationIndexBuildItem index, PanacheEntity } } + /** + * This method produces {@link io.quarkus.security.jpa.SecurityJpa} CDI bean producer for either Hibernate ORM + * or Hibernate Reactive version of the Quarkus Security JPA. + */ + @BuildStep + void registerSecurityJpaImplCdiBean(BuildProducer bytecodeTransformerProducer, + BuildProducer additionalBeanProducer, + Optional securityJpaProviderClassBuildItem) { + if (securityJpaProviderClassBuildItem.isPresent()) { + var item = securityJpaProviderClassBuildItem.get(); + additionalBeanProducer.produce(AdditionalBeanBuildItem.unremovableOf(item.securityJpaProviderClass)); + bytecodeTransformerProducer + .produce(new BytecodeTransformerBuildItem(item.securityJpaProviderClass.getName(), (cls, classVisitor) -> { + var newInstanceUtilMethodDesc = MethodDescriptor.ofMethod(JpaIdentityProviderUtil.class, "newInstance", + Object.class, Class.class); + var classTransformer = new ClassTransformer(cls); + classTransformer.removeMethod("newJpaIdentityProvider", item.jpaIdentityProviderClass); + try (var mc = classTransformer.addMethod("newJpaIdentityProvider", item.jpaIdentityProviderClass)) { + mc.setModifiers(ACC_PRIVATE | ACC_STATIC); + // generates method similar to: + // private static JpaIdentityProvider newJpaIdentityProvider() { + // return JpaIdentityProviderUtil.newInstance(MyEntity__JpaIdentityProviderImpl.class); + // } + var clazz = mc.loadClassFromTCCL(item.jpaIdentityProviderImplName); + // we don't use "new MyEntity__JpaIdentityProviderImpl()" because the generated class needs TCCL + var newInstance = mc.invokeStaticMethod(newInstanceUtilMethodDesc, clazz); + mc.returnValue(newInstance); + } + classTransformer.removeMethod("newJpaTrustedIdentityProvider", item.jpaTrustedIdentityProviderClass); + try (var mc = classTransformer.addMethod("newJpaTrustedIdentityProvider", + item.jpaTrustedIdentityProviderClass)) { + mc.setModifiers(ACC_PRIVATE | ACC_STATIC); + // generates method similar to: + // private static JpaTrustedIdentityProvider newJpaTrustedIdentityProvider() { + // return JpaIdentityProviderUtil.newInstance(MyEntity__JpaTrustedIdentityProviderImpl.class); + // } + var clazz = mc.loadClassFromTCCL(item.jpaTrustedIdentityProviderImplName); + // we don't use "new MyEntity__JpaTrustedIdentityProviderImpl()", + // because the generated class needs TCCL + var newInstance = mc.invokeStaticMethod(newInstanceUtilMethodDesc, clazz); + mc.returnValue(newInstance); + } + return classTransformer.applyTo(classVisitor); + })); + } + } } diff --git a/extensions/security-jpa-common/deployment/src/main/java/io/quarkus/security/jpa/common/deployment/SecurityJpaProviderInfoBuildItem.java b/extensions/security-jpa-common/deployment/src/main/java/io/quarkus/security/jpa/common/deployment/SecurityJpaProviderInfoBuildItem.java new file mode 100644 index 0000000000000..dcf3393c0db04 --- /dev/null +++ b/extensions/security-jpa-common/deployment/src/main/java/io/quarkus/security/jpa/common/deployment/SecurityJpaProviderInfoBuildItem.java @@ -0,0 +1,27 @@ +package io.quarkus.security.jpa.common.deployment; + +import java.util.Objects; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Registers {@link io.quarkus.security.jpa.SecurityJpa} CDI bean producer class and generated identity provider names. + */ +public final class SecurityJpaProviderInfoBuildItem extends SimpleBuildItem { + + final Class securityJpaProviderClass; + final String jpaIdentityProviderImplName; + final String jpaTrustedIdentityProviderImplName; + final Class jpaIdentityProviderClass; + final Class jpaTrustedIdentityProviderClass; + + public SecurityJpaProviderInfoBuildItem(Class securityJpaProviderClass, String jpaIdentityProviderImplName, + String jpaTrustedIdentityProviderImplName, Class jpaIdentityProviderClass, + Class jpaTrustedIdentityProviderClass) { + this.securityJpaProviderClass = Objects.requireNonNull(securityJpaProviderClass); + this.jpaIdentityProviderImplName = jpaIdentityProviderImplName; + this.jpaTrustedIdentityProviderImplName = jpaTrustedIdentityProviderImplName; + this.jpaIdentityProviderClass = jpaIdentityProviderClass; + this.jpaTrustedIdentityProviderClass = jpaTrustedIdentityProviderClass; + } +} 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..dffe34d7cd14a --- /dev/null +++ b/extensions/security-jpa-common/runtime/src/main/java/io/quarkus/security/jpa/SecurityJpa.java @@ -0,0 +1,76 @@ +package io.quarkus.security.jpa; + +import io.quarkus.arc.Arc; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.request.TrustedAuthenticationRequest; +import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; +import io.smallrye.common.annotation.Experimental; + +/** + * A CDI bean used to build Quarkus Security JPA {@link IdentityProvider}s programmatically. + * This bean should be used together with the CDI event 'HttpSecurity' when you + * want to configure the Basic or Form authentication to use the Quarkus Security {@link IdentityProvider}. + */ +@Experimental("This API is currently experimental and might get changed") +public interface SecurityJpa { + + /** + * Creates new {@link IdentityProvider} for the {@link UsernamePasswordAuthenticationRequest} request. + * + * @param persistenceUnitName persistence unit name or null + * @return new {@link IdentityProvider} + */ + IdentityProvider createJpaIdentityProvider(String persistenceUnitName); + + /** + * Creates new {@link IdentityProvider} for the {@link TrustedAuthenticationRequest} request. + * + * @param persistenceUnitName persistence unit name or null + * @return new {@link IdentityProvider} + */ + IdentityProvider createJpaTrustedIdentityProvider(String persistenceUnitName); + + /** + * Creates new {@link IdentityProvider} for the {@link UsernamePasswordAuthenticationRequest} request and + * the default persistence unit. + * + * @return new {@link IdentityProvider} + */ + static IdentityProvider jpa() { + return getInstance().createJpaIdentityProvider(null); + } + + /** + * Creates new {@link IdentityProvider} for the {@link UsernamePasswordAuthenticationRequest} request. + * + * @param persistenceUnitName persistence unit name + * @return new {@link IdentityProvider} + */ + static IdentityProvider jpa(String persistenceUnitName) { + return getInstance().createJpaIdentityProvider(persistenceUnitName); + } + + /** + * Creates new {@link IdentityProvider} for the {@link TrustedAuthenticationRequest} request and the default + * persistence unit. + * + * @return new {@link IdentityProvider} + */ + static IdentityProvider jpaTrustedProvider() { + return getInstance().createJpaTrustedIdentityProvider(null); + } + + /** + * Creates new {@link IdentityProvider} for the {@link TrustedAuthenticationRequest} request. + * + * @param persistenceUnitName persistence unit name + * @return new {@link IdentityProvider} + */ + static IdentityProvider jpaTrustedProvider(String persistenceUnitName) { + return getInstance().createJpaTrustedIdentityProvider(persistenceUnitName); + } + + private static SecurityJpa getInstance() { + return Arc.requireContainer().select(SecurityJpa.class).get(); + } +} diff --git a/extensions/security-jpa-common/runtime/src/main/java/io/quarkus/security/jpa/common/runtime/JpaIdentityProviderUtil.java b/extensions/security-jpa-common/runtime/src/main/java/io/quarkus/security/jpa/common/runtime/JpaIdentityProviderUtil.java index 15a3c4710d1c8..9b87b6655a4f7 100644 --- a/extensions/security-jpa-common/runtime/src/main/java/io/quarkus/security/jpa/common/runtime/JpaIdentityProviderUtil.java +++ b/extensions/security-jpa-common/runtime/src/main/java/io/quarkus/security/jpa/common/runtime/JpaIdentityProviderUtil.java @@ -1,5 +1,6 @@ package io.quarkus.security.jpa.common.runtime; +import java.lang.reflect.InvocationTargetException; import java.security.spec.InvalidKeySpecException; import java.util.List; import java.util.UUID; @@ -82,4 +83,13 @@ public static void passwordAction(PasswordType type) { BcryptUtil.bcryptHash(uuid); } } + + @SuppressWarnings("unchecked") + public static T newInstance(Class clazz) { + try { + return (T) clazz.getConstructors()[0].newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } } 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..81c17e18799ff 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 @@ -1,6 +1,7 @@ package io.quarkus.security.jpa.reactive.deployment; import static io.quarkus.gizmo.MethodDescriptor.ofMethod; +import static io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME; import static io.quarkus.security.jpa.common.deployment.JpaSecurityIdentityUtil.buildIdentity; import static io.quarkus.security.jpa.common.deployment.JpaSecurityIdentityUtil.buildTrustedIdentity; @@ -25,21 +26,26 @@ import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.DotName; import org.jboss.jandex.Index; +import org.jboss.logging.Logger; import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; import io.quarkus.deployment.Feature; +import io.quarkus.deployment.GeneratedClassGizmoAdaptor; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.ApplicationIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.FieldDescriptor; import io.quarkus.gizmo.FunctionCreator; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; +import io.quarkus.hibernate.orm.deployment.PersistenceUnitDescriptorBuildItem; import io.quarkus.panache.common.deployment.PanacheEntityClassesBuildItem; import io.quarkus.security.identity.request.TrustedAuthenticationRequest; import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; @@ -47,33 +53,63 @@ import io.quarkus.security.jpa.common.deployment.JpaSecurityDefinition; import io.quarkus.security.jpa.common.deployment.JpaSecurityDefinitionBuildItem; import io.quarkus.security.jpa.common.deployment.PanacheEntityPredicateBuildItem; +import io.quarkus.security.jpa.common.deployment.SecurityJpaProviderInfoBuildItem; import io.quarkus.security.jpa.reactive.runtime.JpaReactiveIdentityProvider; import io.quarkus.security.jpa.reactive.runtime.JpaReactiveTrustedIdentityProvider; +import io.quarkus.security.jpa.reactive.runtime.SecurityJpaReactiveProvider; import io.smallrye.mutiny.Uni; class QuarkusSecurityJpaReactiveProcessor { private static final DotName NATURAL_ID = DotName.createSimple(NaturalId.class.getName()); + private static final Logger LOGGER = Logger.getLogger(QuarkusSecurityJpaReactiveProcessor.class.getName()); @BuildStep FeatureBuildItem feature() { return new FeatureBuildItem(Feature.SECURITY_JPA_REACTIVE); } + @BuildStep + void registerSecurityJpaProviderClass(Optional jpaSecurityDefinitionBuildItem, + BuildProducer securityJpaProviderClassProducer) { + if (jpaSecurityDefinitionBuildItem.isPresent()) { + var definition = jpaSecurityDefinitionBuildItem.get().get(); + securityJpaProviderClassProducer.produce(new SecurityJpaProviderInfoBuildItem(SecurityJpaReactiveProvider.class, + getJpaIdentityProviderName(definition), getTrustedIdentityProviderName(definition), + JpaReactiveIdentityProvider.class, JpaReactiveTrustedIdentityProvider.class)); + } + } + @BuildStep void configureJpaAuthConfig(ApplicationIndexBuildItem index, BuildProducer beanProducer, Optional jpaSecurityDefinitionBuildItem, - PanacheEntityPredicateBuildItem panacheEntityPredicate) { + PanacheEntityPredicateBuildItem panacheEntityPredicate, + BuildProducer classProducer, + List puDescriptors) { if (jpaSecurityDefinitionBuildItem.isPresent()) { JpaSecurityDefinition jpaSecurityDefinition = jpaSecurityDefinitionBuildItem.get().get(); + // if there is no default persistence unit, the session factory injection into the identity providers + // would fail as there is no qualifier, and we are yet to know which unit does user want + final boolean isDefaultPersistenceUnitAvailable = puDescriptors.stream() + .map(PersistenceUnitDescriptorBuildItem::getPersistenceUnitName) + .anyMatch(DEFAULT_PERSISTENCE_UNIT_NAME::equals); + if (isDefaultPersistenceUnitAvailable) { + LOGGER.debug("Not generating identity provider CDI beans as the default persistence unit is not available." + + " Please either configure the default persistence unit" + + " or use programmatic API to select correct persistence unit name"); + } + + var classOutput = createClassOutput(beanProducer, classProducer, isDefaultPersistenceUnitAvailable); generateIdentityProvider(index.getIndex(), jpaSecurityDefinition, jpaSecurityDefinition.passwordType(), - jpaSecurityDefinition.customPasswordProvider(), beanProducer, panacheEntityPredicate); + jpaSecurityDefinition.customPasswordProvider(), classOutput, panacheEntityPredicate, + isDefaultPersistenceUnitAvailable); + classOutput = createClassOutput(beanProducer, classProducer, isDefaultPersistenceUnitAvailable); generateTrustedIdentityProvider(index.getIndex(), jpaSecurityDefinition, - beanProducer, panacheEntityPredicate); + classOutput, panacheEntityPredicate, isDefaultPersistenceUnitAvailable); } } @@ -92,16 +128,20 @@ private static Set collectPanacheEntities(List beanProducer, PanacheEntityPredicateBuildItem panacheEntityPredicate) { - GeneratedBeanGizmoAdaptor gizmoAdaptor = new GeneratedBeanGizmoAdaptor(beanProducer); + ClassOutput classOutput, PanacheEntityPredicateBuildItem panacheEntityPredicate, + boolean registerProviderAsCdiBean) { - String name = jpaSecurityDefinition.annotatedClass.name() + "__JpaReactiveIdentityProviderImpl"; + String name = getJpaIdentityProviderName(jpaSecurityDefinition); try (ClassCreator classCreator = ClassCreator.builder() .className(name) .superClass(JpaReactiveIdentityProvider.class) - .classOutput(gizmoAdaptor) + .classOutput(classOutput) .build()) { - classCreator.addAnnotation(Singleton.class); + + if (registerProviderAsCdiBean) { + classCreator.addAnnotation(Singleton.class); + } + FieldDescriptor passwordProviderField = classCreator.getFieldCreator("passwordProvider", PasswordProvider.class) .setModifiers(0) // removes default modifier => makes field package-private .getFieldDescriptor(); @@ -127,17 +167,24 @@ private static void generateIdentityProvider(Index index, JpaSecurityDefinition } } + private static String getJpaIdentityProviderName(JpaSecurityDefinition jpaSecurityDefinition) { + return jpaSecurityDefinition.annotatedClass.name() + "__JpaReactiveIdentityProviderImpl"; + } + private static void generateTrustedIdentityProvider(Index index, JpaSecurityDefinition jpaSecurityDefinition, - BuildProducer beanProducer, PanacheEntityPredicateBuildItem panacheEntityPredicate) { - GeneratedBeanGizmoAdaptor gizmoAdaptor = new GeneratedBeanGizmoAdaptor(beanProducer); + ClassOutput classOutput, PanacheEntityPredicateBuildItem panacheEntityPredicate, + boolean registerProviderAsCdiBean) { - String name = jpaSecurityDefinition.annotatedClass.name() + "__JpaReactiveTrustedIdentityProviderImpl"; + String name = getTrustedIdentityProviderName(jpaSecurityDefinition); try (ClassCreator classCreator = ClassCreator.builder() .className(name) .superClass(JpaReactiveTrustedIdentityProvider.class) - .classOutput(gizmoAdaptor) + .classOutput(classOutput) .build()) { - classCreator.addAnnotation(Singleton.class); + + if (registerProviderAsCdiBean) { + classCreator.addAnnotation(Singleton.class); + } MethodDescriptor methodToImpl = MethodDescriptor.ofMethod(JpaReactiveTrustedIdentityProvider.class, "authenticate", Uni.class, Mutiny.Session.class, TrustedAuthenticationRequest.class); @@ -160,6 +207,10 @@ private static void generateTrustedIdentityProvider(Index index, JpaSecurityDefi } } + private static String getTrustedIdentityProviderName(JpaSecurityDefinition jpaSecurityDefinition) { + return jpaSecurityDefinition.annotatedClass.name() + "__JpaReactiveTrustedIdentityProviderImpl"; + } + private static ResultHandle lookupUserById(JpaSecurityDefinition jpaSecurityDefinition, MethodCreator methodCreator, ResultHandle username) { @@ -256,4 +307,10 @@ private static ResultHandle uniLambda(BytecodeCreator creator, ResultHandle uniI return creator.invokeInterfaceMethod(ofMethod(Uni.class, name, Uni.class, Function.class), uniInstance, lambda.getInstance()); } + + private static ClassOutput createClassOutput(BuildProducer beanProducer, + BuildProducer classProducer, boolean registerProviderAsCdiBean) { + return registerProviderAsCdiBean ? new GeneratedBeanGizmoAdaptor(beanProducer) + : new GeneratedClassGizmoAdaptor(classProducer, true); + } } 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..dae1cef161adf --- /dev/null +++ b/extensions/security-jpa-reactive/deployment/src/test/java/io/quarkus/security/jpa/reactive/SecurityJpaReactiveFluentApiTest.java @@ -0,0 +1,159 @@ +package io.quarkus.security.jpa.reactive; + +import static io.quarkus.security.jpa.SecurityJpa.jpaTrustedProvider; +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; + +class SecurityJpaReactiveFluentApiTest { + + @RegisterExtension + static final 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 jpa = SecurityJpa.jpa(); + 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") + .identityProviders(jpa, jpaTrustedProvider()) + .build(); + httpSecurity.mechanism(form).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/deployment/src/test/java/io/quarkus/security/jpa/reactive/SecurityJpaReactiveNamedPersistenceUnitFluentApiTest.java b/extensions/security-jpa-reactive/deployment/src/test/java/io/quarkus/security/jpa/reactive/SecurityJpaReactiveNamedPersistenceUnitFluentApiTest.java new file mode 100644 index 0000000000000..0454a3f558ae0 --- /dev/null +++ b/extensions/security-jpa-reactive/deployment/src/test/java/io/quarkus/security/jpa/reactive/SecurityJpaReactiveNamedPersistenceUnitFluentApiTest.java @@ -0,0 +1,171 @@ +package io.quarkus.security.jpa.reactive; + +import static io.quarkus.security.jpa.SecurityJpa.jpa; +import static io.quarkus.security.jpa.SecurityJpa.jpaTrustedProvider; +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.reactive.other.OtherMinimalUserEntity; +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; + +class SecurityJpaReactiveNamedPersistenceUnitFluentApiTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(() -> ShrinkWrap + .create(JavaArchive.class) + .addClasses(TestApplication.class, OtherMinimalUserEntity.class, SecurityJpaConfiguration.class, + SingleRoleSecuredResource.class, FailingIdentityProvider.class) + .addAsResource("minimal-config/import.sql", "import.sql") + .addAsResource(new StringAsset(""" + quarkus.datasource.named.db-kind=postgresql + quarkus.datasource.named.username=${postgres.reactive.username} + quarkus.datasource.named.password=${postgres.reactive.password} + quarkus.datasource.named.reactive=true + quarkus.datasource.named.reactive.url=${postgres.reactive.url} + quarkus.hibernate-orm.named.packages=io.quarkus.security.jpa.reactive.other + quarkus.hibernate-orm.named.sql-load-script=import.sql + quarkus.hibernate-orm.named.schema-management.strategy=drop-and-create + quarkus.hibernate-orm.named.datasource=named + quarkus.datasource.other-named.db-kind=postgresql + quarkus.datasource.other-named.username=${postgres.reactive.username} + quarkus.datasource.other-named.password=${postgres.reactive.password} + quarkus.datasource.other-named.reactive=true + quarkus.datasource.other-named.reactive.url=${postgres.reactive.url} + quarkus.hibernate-orm.other-named.datasource=other-named + quarkus.hibernate-orm.other-named.packages=io.quarkus.security.jpa.reactive.other + """), "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("/jaxrs-secured/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("/jaxrs-secured/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.enableLoggingOfRequestAndResponseIfValidationFails(); + 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")); + } + + 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") + .identityProviders(jpa("named"), jpaTrustedProvider("named")) + .build(); + httpSecurity + .mechanism(form) + .basic(jpa("other-named")); + } + + } + + @ApplicationScoped + 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/deployment/src/test/java/io/quarkus/security/jpa/reactive/other/OtherMinimalUserEntity.java b/extensions/security-jpa-reactive/deployment/src/test/java/io/quarkus/security/jpa/reactive/other/OtherMinimalUserEntity.java new file mode 100644 index 0000000000000..84939afc865b4 --- /dev/null +++ b/extensions/security-jpa-reactive/deployment/src/test/java/io/quarkus/security/jpa/reactive/other/OtherMinimalUserEntity.java @@ -0,0 +1,33 @@ +package io.quarkus.security.jpa.reactive.other; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import io.quarkus.security.jpa.Password; +import io.quarkus.security.jpa.PasswordType; +import io.quarkus.security.jpa.Roles; +import io.quarkus.security.jpa.UserDefinition; +import io.quarkus.security.jpa.Username; + +@UserDefinition +@Table(name = "test_user") +@Entity +public class OtherMinimalUserEntity { + @Id + @GeneratedValue + public Long id; + + @Column(name = "username") + @Username + public String name; + + @Column(name = "password") + @Password(PasswordType.CLEAR) + public String pass; + + @Roles + public String role; +} diff --git a/extensions/security-jpa-reactive/deployment/src/test/resources/minimal-config/import.sql b/extensions/security-jpa-reactive/deployment/src/test/resources/minimal-config/import.sql index a3c7c7a3a12e3..f87c8dce3c589 100644 --- a/extensions/security-jpa-reactive/deployment/src/test/resources/minimal-config/import.sql +++ b/extensions/security-jpa-reactive/deployment/src/test/resources/minimal-config/import.sql @@ -1,3 +1,4 @@ INSERT INTO test_user (id, username, password, role) VALUES (1, 'admin', 'admin', 'admin'); INSERT INTO test_user (id, username, password, role) VALUES (2, 'user','user', 'user'); INSERT INTO test_user (id, username, password, role) VALUES (3, 'noRoleUser','noRoleUser', ''); +CREATE DATABASE other; diff --git a/extensions/security-jpa-reactive/runtime/src/main/java/io/quarkus/security/jpa/reactive/runtime/SecurityJpaReactiveProvider.java b/extensions/security-jpa-reactive/runtime/src/main/java/io/quarkus/security/jpa/reactive/runtime/SecurityJpaReactiveProvider.java new file mode 100644 index 0000000000000..e1b05d14be614 --- /dev/null +++ b/extensions/security-jpa-reactive/runtime/src/main/java/io/quarkus/security/jpa/reactive/runtime/SecurityJpaReactiveProvider.java @@ -0,0 +1,61 @@ +package io.quarkus.security.jpa.reactive.runtime; + +import static io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME; + +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; + +import org.hibernate.SessionFactory; +import org.hibernate.reactive.mutiny.Mutiny; + +import io.quarkus.hibernate.orm.PersistenceUnit; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.request.TrustedAuthenticationRequest; +import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; +import io.quarkus.security.jpa.SecurityJpa; + +public class SecurityJpaReactiveProvider { + + @Produces + SecurityJpa createSecurityJpa(@Any Instance sessionFactoryInstance) { + return new SecurityJpa() { + @Override + public IdentityProvider createJpaIdentityProvider( + String persistenceUnitName) { + var jpaIdentityProvider = newJpaIdentityProvider(); + jpaIdentityProvider.sessionFactory = getSessionFactoryInstance(persistenceUnitName); + return jpaIdentityProvider; + } + + @Override + public IdentityProvider createJpaTrustedIdentityProvider(String persistenceUnitName) { + var trustedIdentityProvider = newJpaTrustedIdentityProvider(); + trustedIdentityProvider.sessionFactory = getSessionFactoryInstance(persistenceUnitName); + return trustedIdentityProvider; + } + + private Mutiny.SessionFactory getSessionFactoryInstance(String persistenceUnitName) { + final Mutiny.SessionFactory sessionFactory; + if (persistenceUnitName == null || DEFAULT_PERSISTENCE_UNIT_NAME.equals(persistenceUnitName)) { + if (!sessionFactoryInstance.isResolvable()) { + throw new IllegalArgumentException("Default persistence unit is not available"); + } + sessionFactory = sessionFactoryInstance.get(); + } else { + var namedSessionFactoryInstance = sessionFactoryInstance + .select(new PersistenceUnit.PersistenceUnitLiteral(persistenceUnitName)); + if (!namedSessionFactoryInstance.isResolvable()) { + throw new IllegalArgumentException("Unknown persistence unit '%s'".formatted(persistenceUnitName)); + } + sessionFactory = namedSessionFactoryInstance.get(); + } + return sessionFactory; + } + }; + } + + private static native JpaReactiveIdentityProvider newJpaIdentityProvider(); + + private static native JpaReactiveTrustedIdentityProvider newJpaTrustedIdentityProvider(); +} 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..aea7f9c383640 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 @@ -25,18 +25,22 @@ import org.jboss.jandex.DotName; import org.jboss.jandex.Index; import org.jboss.jandex.Type; +import org.jboss.logging.Logger; import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; import io.quarkus.arc.deployment.InjectionPointTransformerBuildItem; import io.quarkus.arc.processor.InjectionPointsTransformer; import io.quarkus.deployment.Feature; +import io.quarkus.deployment.GeneratedClassGizmoAdaptor; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.ApplicationIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.gizmo.AssignableResultHandle; import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.FieldDescriptor; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; @@ -53,8 +57,10 @@ import io.quarkus.security.jpa.common.deployment.JpaSecurityDefinition; import io.quarkus.security.jpa.common.deployment.JpaSecurityDefinitionBuildItem; import io.quarkus.security.jpa.common.deployment.PanacheEntityPredicateBuildItem; +import io.quarkus.security.jpa.common.deployment.SecurityJpaProviderInfoBuildItem; import io.quarkus.security.jpa.runtime.JpaIdentityProvider; import io.quarkus.security.jpa.runtime.JpaTrustedIdentityProvider; +import io.quarkus.security.jpa.runtime.SecurityJpaProvider; class QuarkusSecurityJpaProcessor { @@ -64,31 +70,63 @@ class QuarkusSecurityJpaProcessor { private static final DotName JPA_TRUSTED_IDENTITY_PROVIDER_NAME = DotName .createSimple(JpaTrustedIdentityProvider.class.getName()); private static final DotName PERSISTENCE_UNIT_NAME = DotName.createSimple(PersistenceUnit.class.getName()); + private static final Logger LOGGER = Logger.getLogger(QuarkusSecurityJpaProcessor.class.getName()); @BuildStep FeatureBuildItem feature() { return new FeatureBuildItem(Feature.SECURITY_JPA); } + @BuildStep + void registerSecurityJpaProviderClass(Optional jpaSecurityDefinitionBuildItem, + BuildProducer securityJpaProviderClassProducer) { + if (jpaSecurityDefinitionBuildItem.isPresent()) { + var definition = jpaSecurityDefinitionBuildItem.get().get(); + securityJpaProviderClassProducer.produce(new SecurityJpaProviderInfoBuildItem(SecurityJpaProvider.class, + getJpaIdentityProviderClassName(definition), getTrustedIdentityProviderName(definition), + JpaIdentityProvider.class, JpaTrustedIdentityProvider.class)); + } + } + @BuildStep void configureJpaAuthConfig(ApplicationIndexBuildItem index, List puDescriptors, BuildProducer beanProducer, SecurityJpaBuildTimeConfig secJpaConfig, Optional jpaSecurityDefinitionBuildItem, - PanacheEntityPredicateBuildItem panacheEntityPredicate) { + PanacheEntityPredicateBuildItem panacheEntityPredicate, BuildProducer classProducer) { if (jpaSecurityDefinitionBuildItem.isPresent()) { - final boolean requireActiveCDIRequestContext = shouldActivateCDIReqCtx(puDescriptors, secJpaConfig); + var descriptor = findPersistenceUnitDescriptor(secJpaConfig, puDescriptors); + final boolean registerProviderAsCdiBean; + if (descriptor == null) { + LOGGER.debug("Not generating identity provider CDI beans as the default persistence unit is not available." + + " Please either configure the 'quarkus.security-jpa.persistence-unit-name' configuration property" + + " or use programmatic API to select correct persistence unit name"); + registerProviderAsCdiBean = false; + } else { + registerProviderAsCdiBean = true; + } + + final boolean requireActiveCDIRequestContext = shouldActivateCDIReqCtx(descriptor, secJpaConfig); JpaSecurityDefinition jpaSecurityDefinition = jpaSecurityDefinitionBuildItem.get().get(); generateIdentityProvider(index.getIndex(), jpaSecurityDefinition, jpaSecurityDefinition.passwordType(), - jpaSecurityDefinition.customPasswordProvider(), beanProducer, panacheEntityPredicate, - requireActiveCDIRequestContext); + jpaSecurityDefinition.customPasswordProvider(), + createClassOutput(beanProducer, classProducer, registerProviderAsCdiBean), panacheEntityPredicate, + requireActiveCDIRequestContext, registerProviderAsCdiBean); generateTrustedIdentityProvider(index.getIndex(), jpaSecurityDefinition, - beanProducer, panacheEntityPredicate, requireActiveCDIRequestContext); + createClassOutput(beanProducer, classProducer, registerProviderAsCdiBean), panacheEntityPredicate, + requireActiveCDIRequestContext, registerProviderAsCdiBean); + } } + private static ClassOutput createClassOutput(BuildProducer beanProducer, + BuildProducer classProducer, boolean registerProviderAsCdiBean) { + return registerProviderAsCdiBean ? new GeneratedBeanGizmoAdaptor(beanProducer) + : new GeneratedClassGizmoAdaptor(classProducer, true); + } + @BuildStep(onlyIf = EnabledIfNonDefaultPersistenceUnit.class) InjectionPointTransformerBuildItem transformer(SecurityJpaBuildTimeConfig config) { return new InjectionPointTransformerBuildItem(new InjectionPointsTransformer() { @@ -128,17 +166,20 @@ private Set collectPanacheEntities(List p private void generateIdentityProvider(Index index, JpaSecurityDefinition jpaSecurityDefinition, AnnotationValue passwordTypeValue, AnnotationValue passwordProviderValue, - BuildProducer beanProducer, PanacheEntityPredicateBuildItem panacheEntityPredicate, - boolean requireActiveCDIRequestContext) { - GeneratedBeanGizmoAdaptor gizmoAdaptor = new GeneratedBeanGizmoAdaptor(beanProducer); + ClassOutput classOutput, PanacheEntityPredicateBuildItem panacheEntityPredicate, + boolean requireActiveCDIRequestContext, boolean registerProviderAsCdiBean) { - String name = jpaSecurityDefinition.annotatedClass.name() + "__JpaIdentityProviderImpl"; + String name = getJpaIdentityProviderClassName(jpaSecurityDefinition); try (ClassCreator classCreator = ClassCreator.builder() .className(name) .superClass(JpaIdentityProvider.class) - .classOutput(gizmoAdaptor) + .classOutput(classOutput) .build()) { - classCreator.addAnnotation(Singleton.class); + + if (registerProviderAsCdiBean) { + classCreator.addAnnotation(Singleton.class); + } + FieldDescriptor passwordProviderField = classCreator.getFieldCreator("passwordProvider", PasswordProvider.class) .setModifiers(Modifier.PRIVATE) .getFieldDescriptor(); @@ -170,18 +211,25 @@ private void generateIdentityProvider(Index index, JpaSecurityDefinition jpaSecu } } + private static String getJpaIdentityProviderClassName(JpaSecurityDefinition jpaSecurityDefinition) { + return jpaSecurityDefinition.annotatedClass.name() + "__JpaIdentityProviderImpl"; + } + private void generateTrustedIdentityProvider(Index index, JpaSecurityDefinition jpaSecurityDefinition, - BuildProducer beanProducer, PanacheEntityPredicateBuildItem panacheEntityPredicate, - boolean requireActiveCDIRequestContext) { - GeneratedBeanGizmoAdaptor gizmoAdaptor = new GeneratedBeanGizmoAdaptor(beanProducer); + ClassOutput classOutput, PanacheEntityPredicateBuildItem panacheEntityPredicate, + boolean requireActiveCDIRequestContext, boolean registerProviderAsCdiBean) { - String name = jpaSecurityDefinition.annotatedClass.name() + "__JpaTrustedIdentityProviderImpl"; + String name = getTrustedIdentityProviderName(jpaSecurityDefinition); try (ClassCreator classCreator = ClassCreator.builder() .className(name) .superClass(JpaTrustedIdentityProvider.class) - .classOutput(gizmoAdaptor) + .classOutput(classOutput) .build()) { - classCreator.addAnnotation(Singleton.class); + + if (registerProviderAsCdiBean) { + classCreator.addAnnotation(Singleton.class); + } + try (MethodCreator methodCreator = classCreator.getMethodCreator("authenticate", SecurityIdentity.class, EntityManager.class, TrustedAuthenticationRequest.class)) { methodCreator.setModifiers(Modifier.PUBLIC); @@ -209,6 +257,10 @@ private void generateTrustedIdentityProvider(Index index, JpaSecurityDefinition } } + private static String getTrustedIdentityProviderName(JpaSecurityDefinition jpaSecurityDefinition) { + return jpaSecurityDefinition.annotatedClass.name() + "__JpaTrustedIdentityProviderImpl"; + } + private ResultHandle lookupUserById(JpaSecurityDefinition jpaSecurityDefinition, String name, MethodCreator methodCreator, ResultHandle username, AnnotationInstance naturalIdAnnotation) { ResultHandle user; @@ -257,20 +309,31 @@ private static void activateCDIRequestContext(ClassCreator classCreator) { } } - private static boolean shouldActivateCDIReqCtx(List puDescriptors, + private static boolean shouldActivateCDIReqCtx(PersistenceUnitDescriptorBuildItem descriptor, SecurityJpaBuildTimeConfig secJpaConfig) { - var descriptor = puDescriptors.stream() - .filter(desc -> secJpaConfig.persistenceUnitName().equals(desc.getPersistenceUnitName())).findFirst(); - if (descriptor.isEmpty()) { - throw new ConfigurationException("Persistence unit '" + secJpaConfig.persistenceUnitName() - + "' specified with the 'quarkus.security-jpa.persistence-unit-name' configuration property" - + " does not exist. Please set valid persistence unit name."); + if (descriptor == null) { + // we cannot determine correct value for the programmatic setup before hand + return true; } // 'io.quarkus.hibernate.orm.runtime.tenant.TenantResolver' is only resolved when CDI request context is active // we need to active request context even when TenantResolver is @ApplicationScoped for tenant to be set // see io.quarkus.hibernate.orm.runtime.tenant.HibernateCurrentTenantIdentifierResolver.resolveCurrentTenantIdentifier // for more information - return descriptor.get().getConfig().getMultiTenancyStrategy() != MultiTenancyStrategy.NONE; + return descriptor.getConfig().getMultiTenancyStrategy() != MultiTenancyStrategy.NONE; + } + + private static PersistenceUnitDescriptorBuildItem findPersistenceUnitDescriptor(SecurityJpaBuildTimeConfig secJpaConfig, + List puDescriptors) { + var descriptor = puDescriptors.stream() + .filter(desc -> secJpaConfig.persistenceUnitName().equals(desc.getPersistenceUnitName())) + .findFirst() + .orElse(null); + if (descriptor == null && !DEFAULT_PERSISTENCE_UNIT_NAME.equals(secJpaConfig.persistenceUnitName())) { + throw new ConfigurationException("Persistence unit '" + secJpaConfig.persistenceUnitName() + + "' specified with the 'quarkus.security-jpa.persistence-unit-name' configuration property" + + " does not exist. Please set valid persistence unit name."); + } + return descriptor; } static final class EnabledIfNonDefaultPersistenceUnit implements BooleanSupplier { 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..322e6e323c957 --- /dev/null +++ b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/SecurityJpaFluentApiTest.java @@ -0,0 +1,161 @@ +package io.quarkus.security.jpa; + +import static io.quarkus.security.jpa.SecurityJpa.jpa; +import static io.quarkus.security.jpa.SecurityJpa.jpaTrustedProvider; +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; + +class SecurityJpaFluentApiTest { + + @RegisterExtension + static final 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")); + } + + 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") + .identityProviders(jpa(), jpaTrustedProvider()) + .build(); + httpSecurity + .mechanism(form) + .basic(jpa()); + } + + } + + @ApplicationScoped + 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/deployment/src/test/java/io/quarkus/security/jpa/SecurityJpaNamedPersistenceUnitFluentApiTest.java b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/SecurityJpaNamedPersistenceUnitFluentApiTest.java new file mode 100644 index 0000000000000..b775478ab4a4e --- /dev/null +++ b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/SecurityJpaNamedPersistenceUnitFluentApiTest.java @@ -0,0 +1,181 @@ +package io.quarkus.security.jpa; + +import static io.quarkus.security.jpa.SecurityJpa.jpa; +import static io.quarkus.security.jpa.SecurityJpa.jpaTrustedProvider; +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.other.OtherMinimalUserEntity; +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; + +class SecurityJpaNamedPersistenceUnitFluentApiTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(() -> ShrinkWrap + .create(JavaArchive.class) + .addClasses(SingleRoleSecuredServlet.class, OtherMinimalUserEntity.class, SecurityJpaConfiguration.class, + FailingIdentityProvider.class) + .addAsResource("minimal-config/import.sql", "import.sql") + .addAsResource("minimal-config/import-one-user.sql", "import-one-user.sql") + .addAsResource(new StringAsset(""" + quarkus.datasource.named.db-kind=h2 + quarkus.datasource.named.username=sa + quarkus.datasource.named.password=sa + quarkus.datasource.named.jdbc.url=jdbc:h2:mem:minimal-config-1 + quarkus.hibernate-orm.named.sql-load-script=import.sql + quarkus.hibernate-orm.named.schema-management.strategy=drop-and-create + quarkus.hibernate-orm.named.packages=io.quarkus.security.jpa.other + quarkus.hibernate-orm.named.datasource=named + quarkus.datasource.other-named.db-kind=h2 + quarkus.datasource.other-named.username=sa + quarkus.datasource.other-named.password=sa + quarkus.datasource.other-named.jdbc.url=jdbc:h2:mem:minimal-config-2 + quarkus.hibernate-orm.other-named.sql-load-script=import-one-user.sql + quarkus.hibernate-orm.other-named.schema-management.strategy=drop-and-create + quarkus.hibernate-orm.other-named.packages=io.quarkus.security.jpa.other + quarkus.hibernate-orm.other-named.datasource=other-named + """), "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); + // these are correct credentials for persistence unit 'named', but not for the persistence unit 'other-named' + // and the credentials worked for the form-authentication in the other test method of this class + RestAssured + .given() + .auth().preemptive().basic("user", "user") + .get("/servlet-secured") + .then() + .statusCode(401); + // now use correct credentials for persistence unit 'other-named' + RestAssured + .given() + .auth().preemptive().basic("robin", "robin") + .get("/servlet-secured") + .then() + .statusCode(200) + .body(equalTo("A secured message")); + } + + 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") + .identityProviders(jpa("named"), jpaTrustedProvider("named")) + .build(); + httpSecurity + .mechanism(form) + .basic(jpa("other-named")); + } + + } + + @ApplicationScoped + 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/deployment/src/test/java/io/quarkus/security/jpa/other/OtherMinimalUserEntity.java b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/other/OtherMinimalUserEntity.java new file mode 100644 index 0000000000000..8c30b30601c27 --- /dev/null +++ b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/other/OtherMinimalUserEntity.java @@ -0,0 +1,33 @@ +package io.quarkus.security.jpa.other; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import io.quarkus.security.jpa.Password; +import io.quarkus.security.jpa.PasswordType; +import io.quarkus.security.jpa.Roles; +import io.quarkus.security.jpa.UserDefinition; +import io.quarkus.security.jpa.Username; + +@UserDefinition +@Table(name = "test_user") +@Entity +public class OtherMinimalUserEntity { + @Id + @GeneratedValue + public Long id; + + @Column(name = "username") + @Username + public String name; + + @Column(name = "password") + @Password(PasswordType.CLEAR) + public String pass; + + @Roles + public String role; +} diff --git a/extensions/security-jpa/deployment/src/test/resources/minimal-config/import-one-user.sql b/extensions/security-jpa/deployment/src/test/resources/minimal-config/import-one-user.sql new file mode 100644 index 0000000000000..11b53a81d3e17 --- /dev/null +++ b/extensions/security-jpa/deployment/src/test/resources/minimal-config/import-one-user.sql @@ -0,0 +1 @@ +INSERT INTO test_user (id, username, password, role) VALUES (1, 'robin', 'robin', 'user'); diff --git a/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/runtime/SecurityJpaProvider.java b/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/runtime/SecurityJpaProvider.java new file mode 100644 index 0000000000000..519b071915cf7 --- /dev/null +++ b/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/runtime/SecurityJpaProvider.java @@ -0,0 +1,61 @@ +package io.quarkus.security.jpa.runtime; + +import static io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME; + +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; + +import org.hibernate.SessionFactory; + +import io.quarkus.hibernate.orm.PersistenceUnit.PersistenceUnitLiteral; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.request.TrustedAuthenticationRequest; +import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; +import io.quarkus.security.jpa.SecurityJpa; + +public class SecurityJpaProvider { + + @Produces + SecurityJpa createSecurityJpa(@Any Instance sessionFactoryInstance) { + return new SecurityJpa() { + @Override + public IdentityProvider createJpaIdentityProvider( + String persistenceUnitName) { + var jpaIdentityProvider = newJpaIdentityProvider(); + jpaIdentityProvider.sessionFactory = getSessionFactoryInstance(persistenceUnitName); + return jpaIdentityProvider; + } + + @Override + public IdentityProvider createJpaTrustedIdentityProvider(String persistenceUnitName) { + var trustedIdentityProvider = newJpaTrustedIdentityProvider(); + trustedIdentityProvider.sessionFactory = getSessionFactoryInstance(persistenceUnitName); + return trustedIdentityProvider; + } + + private SessionFactory getSessionFactoryInstance(String persistenceUnitName) { + final SessionFactory sessionFactory; + if (persistenceUnitName == null || DEFAULT_PERSISTENCE_UNIT_NAME.equals(persistenceUnitName)) { + if (!sessionFactoryInstance.isResolvable()) { + throw new IllegalArgumentException("Default persistence unit is not available"); + } + sessionFactory = sessionFactoryInstance.get(); + } else { + var namedSessionFactoryInstance = sessionFactoryInstance + .select(new PersistenceUnitLiteral(persistenceUnitName)); + if (!namedSessionFactoryInstance.isResolvable()) { + throw new IllegalArgumentException("Unknown persistence unit '%s'".formatted(persistenceUnitName)); + } + sessionFactory = namedSessionFactoryInstance.get(); + } + return sessionFactory; + } + }; + } + + private static native JpaIdentityProvider newJpaIdentityProvider(); + + private static native JpaTrustedIdentityProvider newJpaTrustedIdentityProvider(); + +} diff --git a/extensions/security/deployment/pom.xml b/extensions/security/deployment/pom.xml index eb3ceee60ba40..08814492dbf31 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/main/java/io/quarkus/security/deployment/SecurityProcessor.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java index 7ca52db99ccd4..2fe18c31f2822 100644 --- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java @@ -144,6 +144,7 @@ import io.quarkus.security.spi.ClassSecurityCheckStorageBuildItem.ClassStorageBuilder; import io.quarkus.security.spi.CurrentIdentityAssociationClassBuildItem; import io.quarkus.security.spi.DefaultSecurityCheckBuildItem; +import io.quarkus.security.spi.IdentityProviderManagerBuilderBuildItem; import io.quarkus.security.spi.PermissionsAllowedMetaAnnotationBuildItem; import io.quarkus.security.spi.RegisterClassSecurityCheckBuildItem; import io.quarkus.security.spi.RolesAllowedConfigExpResolverBuildItem; @@ -1357,6 +1358,12 @@ void validateRunAsUserUsage(List runAsUserPredicate } } + @Record(ExecutionTime.STATIC_INIT) + @BuildStep + IdentityProviderManagerBuilderBuildItem createIdentityProviderManagerBuilder(SecurityCheckRecorder recorder) { + return new IdentityProviderManagerBuilderBuildItem(recorder.createIdentityProviderManagerBuilder()); + } + private static String toString(MethodInfo mi) { return "%s#%s".formatted(mi.declaringClass().name().toString(), mi.name()); } diff --git a/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/SecurityHandlerConstants.java b/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/SecurityHandlerConstants.java index 551137d9c382f..f88baa39dedef 100644 --- a/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/SecurityHandlerConstants.java +++ b/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/SecurityHandlerConstants.java @@ -1,6 +1,5 @@ package io.quarkus.security.spi.runtime; -import jakarta.annotation.Priority; import jakarta.interceptor.Interceptor; public class SecurityHandlerConstants { 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..63d4dc92d4672 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 @@ -34,8 +34,14 @@ public Executor get() { @Produces @ApplicationScoped - public IdentityProviderManager ipm(Instance> identityProviders, + IdentityProviderManager ipm(Instance> identityProviders, Instance augmentors, BlockingSecurityExecutor blockingExecutor) { + return createIdentityProviderManager(identityProviders, augmentors, blockingExecutor); + } + + static QuarkusIdentityProviderManagerImpl createIdentityProviderManager( + Iterable> identityProviders, Iterable augmentors, + BlockingSecurityExecutor blockingExecutor) { boolean customAnon = false; QuarkusIdentityProviderManagerImpl.Builder builder = QuarkusIdentityProviderManagerImpl.builder(); for (var i : identityProviders) { @@ -53,5 +59,4 @@ public IdentityProviderManager ipm(Instance> identityProvide builder.setBlockingExecutor(blockingExecutor); return builder.build(); } - } diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java index 0d783b4a3405e..2475937ed329b 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java @@ -1,5 +1,6 @@ package io.quarkus.security.runtime; +import static io.quarkus.security.runtime.IdentityProviderManagerCreator.createIdentityProviderManager; import static io.quarkus.security.runtime.QuarkusSecurityRolesAllowedConfigBuilder.transformToKey; import java.lang.invoke.MethodHandle; @@ -9,6 +10,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; @@ -30,6 +32,9 @@ import io.quarkus.security.ForbiddenException; import io.quarkus.security.StringPermission; import io.quarkus.security.identity.CurrentIdentityAssociation; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentityAugmentor; import io.quarkus.security.runtime.interceptor.SecurityCheckStorageBuilder; import io.quarkus.security.runtime.interceptor.SecurityConstrainer; import io.quarkus.security.runtime.interceptor.check.AuthenticatedCheck; @@ -445,4 +450,22 @@ public QuarkusPermissionSecurityIdentityAugmentor apply( } }; } + + public Function>, IdentityProviderManager> createIdentityProviderManagerBuilder() { + return new Function>, IdentityProviderManager>() { + @Override + public IdentityProviderManager apply(Collection> localIdentityProviders) { + final Iterable> identityProviders; + if (localIdentityProviders == null || localIdentityProviders.isEmpty()) { + throw new IllegalStateException("Cannot build IdentityProviderManager without IdentityProviders"); + } else { + identityProviders = localIdentityProviders; + } + var container = Arc.requireContainer(); + var globalAugmentors = container.select(SecurityIdentityAugmentor.class); + var blockingExecutor = container.select(BlockingSecurityExecutor.class).get(); + return createIdentityProviderManager(identityProviders, globalAugmentors, blockingExecutor); + } + }; + } } diff --git a/extensions/security/spi/src/main/java/io/quarkus/security/spi/IdentityProviderManagerBuilderBuildItem.java b/extensions/security/spi/src/main/java/io/quarkus/security/spi/IdentityProviderManagerBuilderBuildItem.java new file mode 100644 index 0000000000000..83a8cf71695bf --- /dev/null +++ b/extensions/security/spi/src/main/java/io/quarkus/security/spi/IdentityProviderManagerBuilderBuildItem.java @@ -0,0 +1,26 @@ +package io.quarkus.security.spi; + +import java.util.Collection; +import java.util.Objects; +import java.util.function.Function; + +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.IdentityProviderManager; + +/** + * Provides a way for core extensions to build their own {@link IdentityProviderManager}. + * Given identity providers must not be null or empty. + */ +public final class IdentityProviderManagerBuilderBuildItem extends SimpleBuildItem { + + private final Function>, IdentityProviderManager> builder; + + public IdentityProviderManagerBuilderBuildItem(Function>, IdentityProviderManager> builder) { + this.builder = Objects.requireNonNull(builder); + } + + public Function>, IdentityProviderManager> getBuilder() { + return builder; + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java index d1f81dd99e62d..18f21431fc3b6 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java @@ -24,6 +24,7 @@ import java.util.Set; import java.util.function.BooleanSupplier; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -74,11 +75,14 @@ import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.spi.AdditionalSecuredMethodsBuildItem; import io.quarkus.security.spi.AdditionalSecurityAnnotationBuildItem; import io.quarkus.security.spi.AdditionalSecurityConstrainerEventPropsBuildItem; import io.quarkus.security.spi.ClassSecurityAnnotationBuildItem; import io.quarkus.security.spi.CurrentIdentityAssociationClassBuildItem; +import io.quarkus.security.spi.IdentityProviderManagerBuilderBuildItem; import io.quarkus.security.spi.RegisterClassSecurityCheckBuildItem; import io.quarkus.security.spi.SecurityTransformer; import io.quarkus.security.spi.SecurityTransformerBuildItem; @@ -330,16 +334,19 @@ void prepareCsrfConfigBuilder(Capabilities capabilities, Optional authenticationHandler, HttpSecurityRecorder recorder, BeanContainerBuildItem beanContainerBuildItem, - ShutdownContextBuildItem shutdown) { + ShutdownContextBuildItem shutdown, + Optional identityProviderManagerBuilderBuildItem) { if (authenticationHandler.isPresent()) { - RuntimeValue programmaticCorsConfig = recorder.prepareHttpSecurityConfiguration(shutdown); + Function>, IdentityProviderManager> identityProviderManagerBuilder = identityProviderManagerBuilderBuildItem + .map(IdentityProviderManagerBuilderBuildItem::getBuilder).orElse(null); + RuntimeValue programmaticCorsConfig = recorder.prepareHttpSecurityConfiguration(shutdown, + identityProviderManagerBuilder); recorder.initializeHttpAuthenticatorHandler(authenticationHandler.get().handler, beanContainerBuildItem.getValue()); return new HttpSecurityConfigSetupCompleteBuildItem(programmaticCorsConfig); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java index 81fba6935f0ef..d5350fc2f6c3b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java @@ -2,6 +2,7 @@ import static io.quarkus.security.spi.runtime.SecurityEventHelper.fire; import static io.quarkus.vertx.http.runtime.security.FormAuthenticationEvent.createLoginEvent; +import static io.quarkus.vertx.http.runtime.security.HttpSecurityImpl.createMechanism; import static io.quarkus.vertx.http.runtime.security.RoutingContextAwareSecurityIdentity.addRoutingCtxToIdentityIfMissing; import java.net.URI; @@ -12,6 +13,7 @@ import java.util.Arrays; import java.util.Base64; import java.util.HashSet; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -29,6 +31,7 @@ import io.quarkus.arc.Arc; import io.quarkus.security.AuthenticationCompletionException; import io.quarkus.security.credential.PasswordCredential; +import io.quarkus.security.identity.IdentityProvider; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.AuthenticationRequest; @@ -404,6 +407,15 @@ public static void logout(RoutingContext routingContext) { routingContext.response().addCookie(cookie); } + public static HttpAuthenticationMechanism of(FormAuthConfig runtimeForm, Optional encKey, + IdentityProvider trustedIdentityProvider, + IdentityProvider usernamePasswordIdentityProvider) { + Objects.requireNonNull(trustedIdentityProvider); + Objects.requireNonNull(usernamePasswordIdentityProvider); + var formBasedMechanism = new FormAuthenticationMechanism(runtimeForm, encKey); + return createMechanism(formBasedMechanism, List.of(trustedIdentityProvider, usernamePasswordIdentityProvider)); + } + private static String startWithSlash(String page) { if (page == null) { 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..af80a8f0377f2 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; @@ -11,6 +12,7 @@ import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.BiPredicate; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -18,7 +20,11 @@ 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.request.AuthenticationRequest; +import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; import io.quarkus.tls.TlsConfiguration; import io.quarkus.tls.TlsConfigurationRegistry; import io.quarkus.vertx.http.runtime.FormAuthConfig; @@ -41,6 +47,7 @@ final class HttpSecurityImpl implements HttpSecurity { private static final Logger LOG = Logger.getLogger(HttpSecurityImpl.class.getName()); + private static volatile Function>, IdentityProviderManager> identityProviderManagerBuilder = null; private final List httpPermissions; private final List mechanisms; @@ -116,20 +123,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 +147,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()) { @@ -173,6 +181,12 @@ public HttpSecurity basic(String authenticationRealm) { return mechanism(Basic.realm(authenticationRealm)); } + @Override + public HttpSecurity basic(IdentityProvider identityProvider) { + Objects.requireNonNull(identityProvider); + return mechanism(createMechanism(Basic.create(), List.of(identityProvider))); + } + @Override public HttpSecurity mTLS() { return mTLS(ClientAuth.REQUIRED); @@ -644,4 +658,64 @@ CORSConfig getCorsConfig() { CSRF getCsrf() { return csrf; } + + 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; + } + + static HttpAuthenticationMechanism createMechanism(HttpAuthenticationMechanism mechanism, + Collection> identityProviders) { + Objects.requireNonNull(identityProviderManagerBuilder, "IdentityProviderManager builder function is not set"); + IdentityProviderManager customIdentityProviderManager = identityProviderManagerBuilder.apply(identityProviders); + return new DelegatingHttpAuthenticationMechanism(mechanism, customIdentityProviderManager); + } + + static void setIdentityProviderManagerBuilder( + Function>, IdentityProviderManager> identityProviderManagerBuilder) { + HttpSecurityImpl.identityProviderManagerBuilder = identityProviderManagerBuilder; + } + + static void clearIdentityProviderManagerBuilder() { + HttpSecurityImpl.identityProviderManagerBuilder = null; + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java index 7025a883c033f..3273a2e2936ea 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java @@ -12,6 +12,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -39,6 +40,8 @@ import io.quarkus.security.AuthenticationException; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.AuthenticationRedirectException; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.AnonymousAuthenticationRequest; import io.quarkus.security.spi.runtime.MethodDescription; @@ -186,10 +189,13 @@ public Map get() { }; } - public RuntimeValue prepareHttpSecurityConfiguration(ShutdownContext shutdownContext) { + public RuntimeValue prepareHttpSecurityConfiguration(ShutdownContext shutdownContext, + Function>, IdentityProviderManager> identityProviderManagerBuilder) { + HttpSecurityImpl.setIdentityProviderManagerBuilder(identityProviderManagerBuilder); // this is done so that we prepare and validate HTTP Security config before the first incoming request var config = HttpSecurityConfiguration.get(httpConfig.getValue(), httpBuildTimeConfig); shutdownContext.addShutdownTask(HttpSecurityConfiguration::clear); + shutdownContext.addShutdownTask(HttpSecurityImpl::clearIdentityProviderManagerBuilder); return new RuntimeValue<>(config.getCorsConfig()); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/security/Form.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/security/Form.java index 15780a19531f1..5ddacaf72272f 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/security/Form.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/security/Form.java @@ -7,6 +7,9 @@ import org.eclipse.microprofile.config.ConfigProvider; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.request.TrustedAuthenticationRequest; +import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; import io.quarkus.vertx.http.runtime.FormAuthConfig; import io.quarkus.vertx.http.runtime.VertxHttpConfig; import io.quarkus.vertx.http.runtime.security.FormAuthenticationMechanism; @@ -61,6 +64,8 @@ final class Builder { private Optional> landingPageQueryParams; private Optional> errorPageQueryParams; private Optional> loginPageQueryParams; + private IdentityProvider trustedIdentityProvider; + private IdentityProvider usernamePasswordIdentityProvider; public Builder() { this(ConfigProvider.getConfig().unwrap(SmallRyeConfig.class).getConfigMapping(VertxHttpConfig.class)); @@ -87,6 +92,8 @@ private Builder(VertxHttpConfig vertxHttpConfig) { this.landingPageQueryParams = formAuthConfig.landingPageQueryParams(); this.errorPageQueryParams = formAuthConfig.errorPageQueryParams(); this.loginPageQueryParams = formAuthConfig.loginPageQueryParams(); + this.usernamePasswordIdentityProvider = null; + this.trustedIdentityProvider = null; } /** @@ -341,8 +348,28 @@ public Builder encryptionKey(String encryptionKey) { return this; } + /** + * Register {@link IdentityProvider}s as the Quarkus Security JPA {@link IdentityProvider}s programmatically. + * If not configured, the {@link IdentityProvider}s globally registered as CDI beans are used instead. + * + * @param trustedIdentityProvider {@link IdentityProvider} for {@link UsernamePasswordAuthenticationRequest} + * @param usernamePasswordIdentityProvider {@link IdentityProvider} for {@link TrustedAuthenticationRequest} + * @return + */ + public Builder identityProviders( + IdentityProvider usernamePasswordIdentityProvider, + IdentityProvider trustedIdentityProvider) { + this.trustedIdentityProvider = Objects.requireNonNull(trustedIdentityProvider); + this.usernamePasswordIdentityProvider = Objects.requireNonNull(usernamePasswordIdentityProvider); + return this; + } + public HttpAuthenticationMechanism build() { - return new FormAuthenticationMechanism(createFormConfig(), encryptionKey); + if (trustedIdentityProvider == null) { + return new FormAuthenticationMechanism(createFormConfig(), encryptionKey); + } + return FormAuthenticationMechanism.of(createFormConfig(), encryptionKey, trustedIdentityProvider, + usernamePasswordIdentityProvider); } private FormAuthConfig createFormConfig() { 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..018d867c6e07e 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 @@ -7,7 +7,9 @@ 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.request.UsernamePasswordAuthenticationRequest; import io.quarkus.tls.TlsConfiguration; import io.quarkus.vertx.http.runtime.VertxHttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.cors.CORSConfig; @@ -144,6 +146,14 @@ public interface HttpSecurity { */ HttpSecurity basic(String authenticationRealm); + /** + * Registers the Basic authentication mechanism in addition to all other global authentication mechanisms. + * + * @param identityProvider such as the Quarkus Security JPA provider; must not be null + * @return HttpSecurity + */ + HttpSecurity basic(IdentityProvider identityProvider); + /** * 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.