Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,7 @@ The same authorization can be required with the `@PermissionsAllowed(value = { "
* xref:security-authentication-mechanisms.adoc#mtls-programmatic-set-up[Set up the mutual TLS client authentication programmatically]
* xref:security-cors.adoc#cors-filter-programmatic-set-up[Configuring the CORS filter programmatically]
* xref:security-csrf-prevention.adoc#csrf-prevention-programmatic-set-up[Configuring the CSRF prevention programmatically]
* xref:security-jpa.adoc#programmatic-set-up[Set up Basic authentication with Jakarta Persistence programmatically]

[[standard-security-annotations]]
== Authorization using annotations
Expand Down
25 changes: 25 additions & 0 deletions docs/src/main/asciidoc/security-jpa.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,31 @@ For more information about proactive authentication, see the Quarkus xref:securi

include::{generated-dir}/config/quarkus-security-jpa.adoc[opts=optional, leveloffset=+2]

[[programmatic-set-up]]
== Set up Basic authentication with Jakarta Persistence programmatically

The `io.quarkus.vertx.http.security.HttpSecurity` CDI event allows to configure the Basic authentication mechanism programmatically.
If there is more than one `IdentityProvider` handling the `io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest` request, you can configure the Basic authentication to use the Jakarta Persistence `IdentityProvider` like in the example below:

[source,java]
----
package org.acme.http.security;

import static io.quarkus.security.jpa.SecurityJpa.jpa;

import jakarta.enterprise.event.Observes;

import io.quarkus.vertx.http.security.HttpSecurity;

public class HttpSecurityConfiguration {

void configure(@Observes HttpSecurity httpSecurity) {
httpSecurity.basic(jpa());
}

}
----

== References

* xref:security-getting-started-tutorial.adoc[Getting started with Security by using Basic authentication and Jakarta Persistence]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.quarkus.security.jpa;

import java.util.Collection;

import io.quarkus.arc.Arc;
import io.quarkus.security.identity.IdentityProvider;

/**
* A CDI beans used to retrieve generated Quarkus Security JPA beans.
*/
public interface SecurityJpa {

/**
* @return Quarkus Security JPA {@link IdentityProvider}s
*/
Collection<IdentityProvider<?>> getIdentityProviders();

/**
* Looks up the {@link SecurityJpa} CDI bean and returns the Quarkus Security JPA {@link IdentityProvider}s.
*
* @return Quarkus Security JPA {@link IdentityProvider}s
*/
static Collection<IdentityProvider<?>> jpa() {
try (var securityJpaInstance = Arc.requireContainer().instance(SecurityJpa.class)) {
return securityJpaInstance.get().getIdentityProviders();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.jboss.jandex.DotName;
import org.jboss.jandex.Index;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
import io.quarkus.deployment.Feature;
Expand All @@ -49,6 +50,7 @@
import io.quarkus.security.jpa.common.deployment.PanacheEntityPredicateBuildItem;
import io.quarkus.security.jpa.reactive.runtime.JpaReactiveIdentityProvider;
import io.quarkus.security.jpa.reactive.runtime.JpaReactiveTrustedIdentityProvider;
import io.quarkus.security.jpa.reactive.runtime.SecurityJpaReactiveImpl;
import io.smallrye.mutiny.Uni;

class QuarkusSecurityJpaReactiveProcessor {
Expand Down Expand Up @@ -82,6 +84,11 @@ PanacheEntityPredicateBuildItem panacheEntityPredicate(List<PanacheEntityClasses
return new PanacheEntityPredicateBuildItem(collectPanacheEntities(panacheEntityClasses));
}

@BuildStep
AdditionalBeanBuildItem registerSecurityJpaReactiveImplCdiBean() {
return AdditionalBeanBuildItem.unremovableOf(SecurityJpaReactiveImpl.class);
}

private static Set<String> collectPanacheEntities(List<PanacheEntityClassesBuildItem> panacheEntityClassesBuildItems) {
Set<String> modelClasses = new HashSet<>();
for (PanacheEntityClassesBuildItem panacheEntityClasses : panacheEntityClassesBuildItems) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package io.quarkus.security.jpa.reactive;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;

import java.time.Duration;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.IdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest;
import io.quarkus.security.jpa.SecurityJpa;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.vertx.http.security.Form;
import io.quarkus.vertx.http.security.HttpSecurity;
import io.restassured.RestAssured;
import io.restassured.filter.cookie.CookieFilter;
import io.smallrye.mutiny.Uni;

public class SecurityJpaReactiveFluentApiTest {

@RegisterExtension
static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(() -> ShrinkWrap
.create(JavaArchive.class)
.addClasses(TestApplication.class, MinimalUserEntity.class, SecurityJpaConfiguration.class,
SingleRoleSecuredResource.class, FailingIdentityProvider.class)
.addAsResource("minimal-config/import.sql", "import.sql")
.addAsResource(new StringAsset("""
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=${postgres.reactive.username}
quarkus.datasource.password=${postgres.reactive.password}
quarkus.datasource.reactive=true
quarkus.datasource.reactive.url=${postgres.reactive.url}
quarkus.hibernate-orm.sql-load-script=import.sql
quarkus.hibernate-orm.schema-management.strategy=drop-and-create
"""), "application.properties"));

@Test
void testFormBasedAuthentication() {
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
CookieFilter cookies = new CookieFilter();
RestAssured
.given()
.filter(cookies)
.redirects().follow(false)
.when()
.get("/jaxrs-secured/user-secured")
.then()
.assertThat()
.statusCode(302)
.header("location", containsString("/login"))
.cookie("quarkus-redirect-location", containsString("/user-secured"));

// test with a non-existent user
RestAssured
.given()
.filter(cookies)
.redirects().follow(false)
.when()
.formParam("j_username", "dummy")
.formParam("j_password", "dummy")
.post("/j_security_check")
.then()
.assertThat()
.statusCode(302);

RestAssured
.given()
.filter(cookies)
.redirects().follow(false)
.when()
.formParam("j_username", "user")
.formParam("j_password", "user")
.post("/j_security_check")
.then()
.assertThat()
.statusCode(302)
.header("location", containsString("/user-secured"))
.cookie("laitnederc-sukrauq", notNullValue());

RestAssured
.given()
.filter(cookies)
.redirects().follow(false)
.when()
.get("/jaxrs-secured/user-secured")
.then()
.assertThat()
.statusCode(200)
.body(equalTo("A secured message"));
}

@Test
void testBasicAuthentication() {
RestAssured
.given()
.auth().preemptive().basic("user", "wrong-password")
.get("/jaxrs-secured/user-secured")
.then()
.statusCode(401);
RestAssured
.given()
.auth().preemptive().basic("user", "user")
.get("/jaxrs-secured/user-secured")
.then()
.statusCode(200)
.body(equalTo("A secured message"));
}

public static class SecurityJpaConfiguration {

void configure(@Observes HttpSecurity httpSecurity) {
var form = Form.builder()
.loginPage("login")
.errorPage("error")
.landingPage("landing")
.cookieName("laitnederc-sukrauq")
.newCookieInterval(Duration.ofSeconds(5))
.timeout(Duration.ofSeconds(5))
.encryptionKey("CHANGEIT-CHANGEIT-CHANGEIT-CHANGEIT-CHANGEIT")
.build();
var jpa = SecurityJpa.jpa();
httpSecurity.mechanism(form, jpa).basic(jpa);
}

}

@ApplicationScoped
public static class FailingIdentityProvider implements IdentityProvider<UsernamePasswordAuthenticationRequest> {

@Override
public Class<UsernamePasswordAuthenticationRequest> getRequestType() {
return UsernamePasswordAuthenticationRequest.class;
}

@Override
public Uni<SecurityIdentity> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.quarkus.security.jpa.reactive.runtime;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;

import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;

import io.quarkus.security.identity.IdentityProvider;
import io.quarkus.security.jpa.SecurityJpa;

public final class SecurityJpaReactiveImpl implements SecurityJpa {

@Inject
Instance<JpaReactiveIdentityProvider> jpaReactiveIdentityProvider;

@Inject
Instance<JpaReactiveTrustedIdentityProvider> jpaReactiveTrustedIdentityProvider;

@Override
public Collection<IdentityProvider<?>> getIdentityProviders() {
final Collection<IdentityProvider<?>> result = new ArrayList<>();
if (jpaReactiveIdentityProvider.isResolvable()) {
result.add(jpaReactiveIdentityProvider.get());
}
if (jpaReactiveTrustedIdentityProvider.isResolvable()) {
result.add(jpaReactiveTrustedIdentityProvider.get());
}
return Collections.unmodifiableCollection(result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.jboss.jandex.Index;
import org.jboss.jandex.Type;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
import io.quarkus.arc.deployment.InjectionPointTransformerBuildItem;
Expand Down Expand Up @@ -55,6 +56,7 @@
import io.quarkus.security.jpa.common.deployment.PanacheEntityPredicateBuildItem;
import io.quarkus.security.jpa.runtime.JpaIdentityProvider;
import io.quarkus.security.jpa.runtime.JpaTrustedIdentityProvider;
import io.quarkus.security.jpa.runtime.SecurityJpaImpl;

class QuarkusSecurityJpaProcessor {

Expand Down Expand Up @@ -118,6 +120,11 @@ PanacheEntityPredicateBuildItem panacheEntityPredicate(List<PanacheEntityClasses
return new PanacheEntityPredicateBuildItem(collectPanacheEntities(panacheEntityClasses));
}

@BuildStep
AdditionalBeanBuildItem registerSecurityJpaImplCdiBean() {
return AdditionalBeanBuildItem.unremovableOf(SecurityJpaImpl.class);
}

private Set<String> collectPanacheEntities(List<PanacheEntityClassesBuildItem> panacheEntityClassesBuildItems) {
Set<String> modelClasses = new HashSet<>();
for (PanacheEntityClassesBuildItem panacheEntityClasses : panacheEntityClassesBuildItems) {
Expand Down
Loading
Loading