Skip to content

Commit 86e5b6a

Browse files
committed
feat(security): add fluent API for Security JPA
1 parent f542c6a commit 86e5b6a

File tree

16 files changed

+929
-17
lines changed

16 files changed

+929
-17
lines changed

docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,7 @@ The same authorization can be required with the `@PermissionsAllowed(value = { "
600600
* xref:security-authentication-mechanisms.adoc#mtls-programmatic-set-up[Set up the mutual TLS client authentication programmatically]
601601
* xref:security-cors.adoc#cors-filter-programmatic-set-up[Configuring the CORS filter programmatically]
602602
* xref:security-csrf-prevention.adoc#csrf-prevention-programmatic-set-up[Configuring the CSRF prevention programmatically]
603+
* xref:security-jpa.adoc#programmatic-set-up[Set up Basic authentication with Jakarta Persistence programmatically]
603604

604605
[[standard-security-annotations]]
605606
== Authorization using annotations

docs/src/main/asciidoc/security-jpa.adoc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,31 @@ For more information about proactive authentication, see the Quarkus xref:securi
222222

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

225+
[[programmatic-set-up]]
226+
== Set up Basic authentication with Jakarta Persistence programmatically
227+
228+
The `io.quarkus.vertx.http.security.HttpSecurity` CDI event allows to configure the Basic authentication mechanism programmatically.
229+
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:
230+
231+
[source,java]
232+
----
233+
package org.acme.http.security;
234+
235+
import static io.quarkus.security.jpa.SecurityJpa.jpa;
236+
237+
import jakarta.enterprise.event.Observes;
238+
239+
import io.quarkus.vertx.http.security.HttpSecurity;
240+
241+
public class HttpSecurityConfiguration {
242+
243+
void configure(@Observes HttpSecurity httpSecurity) {
244+
httpSecurity.basic(jpa());
245+
}
246+
247+
}
248+
----
249+
225250
== References
226251

227252
* xref:security-getting-started-tutorial.adoc[Getting started with Security by using Basic authentication and Jakarta Persistence]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package io.quarkus.security.jpa;
2+
3+
import java.util.Collection;
4+
5+
import io.quarkus.arc.Arc;
6+
import io.quarkus.security.identity.IdentityProvider;
7+
8+
/**
9+
* A CDI beans used to retrieve generated Quarkus Security JPA beans.
10+
*/
11+
public interface SecurityJpa {
12+
13+
/**
14+
* @return Quarkus Security JPA {@link IdentityProvider}s
15+
*/
16+
Collection<IdentityProvider<?>> getIdentityProviders();
17+
18+
/**
19+
* Looks up the {@link SecurityJpa} CDI bean and returns the Quarkus Security JPA {@link IdentityProvider}s.
20+
*
21+
* @return Quarkus Security JPA {@link IdentityProvider}s
22+
*/
23+
static Collection<IdentityProvider<?>> jpa() {
24+
try (var securityJpaInstance = Arc.requireContainer().instance(SecurityJpa.class)) {
25+
return securityJpaInstance.get().getIdentityProviders();
26+
}
27+
}
28+
}

extensions/security-jpa-reactive/deployment/src/main/java/io/quarkus/security/jpa/reactive/deployment/QuarkusSecurityJpaReactiveProcessor.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.jboss.jandex.DotName;
2727
import org.jboss.jandex.Index;
2828

29+
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
2930
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
3031
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
3132
import io.quarkus.deployment.Feature;
@@ -49,6 +50,7 @@
4950
import io.quarkus.security.jpa.common.deployment.PanacheEntityPredicateBuildItem;
5051
import io.quarkus.security.jpa.reactive.runtime.JpaReactiveIdentityProvider;
5152
import io.quarkus.security.jpa.reactive.runtime.JpaReactiveTrustedIdentityProvider;
53+
import io.quarkus.security.jpa.reactive.runtime.SecurityJpaReactiveImpl;
5254
import io.smallrye.mutiny.Uni;
5355

5456
class QuarkusSecurityJpaReactiveProcessor {
@@ -82,6 +84,11 @@ PanacheEntityPredicateBuildItem panacheEntityPredicate(List<PanacheEntityClasses
8284
return new PanacheEntityPredicateBuildItem(collectPanacheEntities(panacheEntityClasses));
8385
}
8486

87+
@BuildStep
88+
AdditionalBeanBuildItem registerSecurityJpaReactiveImplCdiBean() {
89+
return AdditionalBeanBuildItem.unremovableOf(SecurityJpaReactiveImpl.class);
90+
}
91+
8592
private static Set<String> collectPanacheEntities(List<PanacheEntityClassesBuildItem> panacheEntityClassesBuildItems) {
8693
Set<String> modelClasses = new HashSet<>();
8794
for (PanacheEntityClassesBuildItem panacheEntityClasses : panacheEntityClassesBuildItems) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package io.quarkus.security.jpa.reactive;
2+
3+
import static org.hamcrest.Matchers.containsString;
4+
import static org.hamcrest.Matchers.equalTo;
5+
import static org.hamcrest.Matchers.notNullValue;
6+
7+
import java.time.Duration;
8+
9+
import jakarta.enterprise.context.ApplicationScoped;
10+
import jakarta.enterprise.event.Observes;
11+
12+
import org.jboss.shrinkwrap.api.ShrinkWrap;
13+
import org.jboss.shrinkwrap.api.asset.StringAsset;
14+
import org.jboss.shrinkwrap.api.spec.JavaArchive;
15+
import org.junit.jupiter.api.Test;
16+
import org.junit.jupiter.api.extension.RegisterExtension;
17+
18+
import io.quarkus.security.identity.AuthenticationRequestContext;
19+
import io.quarkus.security.identity.IdentityProvider;
20+
import io.quarkus.security.identity.SecurityIdentity;
21+
import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest;
22+
import io.quarkus.security.jpa.SecurityJpa;
23+
import io.quarkus.test.QuarkusUnitTest;
24+
import io.quarkus.vertx.http.security.Form;
25+
import io.quarkus.vertx.http.security.HttpSecurity;
26+
import io.restassured.RestAssured;
27+
import io.restassured.filter.cookie.CookieFilter;
28+
import io.smallrye.mutiny.Uni;
29+
30+
public class SecurityJpaReactiveFluentApiTest {
31+
32+
@RegisterExtension
33+
static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(() -> ShrinkWrap
34+
.create(JavaArchive.class)
35+
.addClasses(TestApplication.class, MinimalUserEntity.class, SecurityJpaConfiguration.class,
36+
SingleRoleSecuredResource.class, FailingIdentityProvider.class)
37+
.addAsResource("minimal-config/import.sql", "import.sql")
38+
.addAsResource(new StringAsset("""
39+
quarkus.datasource.db-kind=postgresql
40+
quarkus.datasource.username=${postgres.reactive.username}
41+
quarkus.datasource.password=${postgres.reactive.password}
42+
quarkus.datasource.reactive=true
43+
quarkus.datasource.reactive.url=${postgres.reactive.url}
44+
quarkus.hibernate-orm.sql-load-script=import.sql
45+
quarkus.hibernate-orm.schema-management.strategy=drop-and-create
46+
"""), "application.properties"));
47+
48+
@Test
49+
void testFormBasedAuthentication() {
50+
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
51+
CookieFilter cookies = new CookieFilter();
52+
RestAssured
53+
.given()
54+
.filter(cookies)
55+
.redirects().follow(false)
56+
.when()
57+
.get("/jaxrs-secured/user-secured")
58+
.then()
59+
.assertThat()
60+
.statusCode(302)
61+
.header("location", containsString("/login"))
62+
.cookie("quarkus-redirect-location", containsString("/user-secured"));
63+
64+
// test with a non-existent user
65+
RestAssured
66+
.given()
67+
.filter(cookies)
68+
.redirects().follow(false)
69+
.when()
70+
.formParam("j_username", "dummy")
71+
.formParam("j_password", "dummy")
72+
.post("/j_security_check")
73+
.then()
74+
.assertThat()
75+
.statusCode(302);
76+
77+
RestAssured
78+
.given()
79+
.filter(cookies)
80+
.redirects().follow(false)
81+
.when()
82+
.formParam("j_username", "user")
83+
.formParam("j_password", "user")
84+
.post("/j_security_check")
85+
.then()
86+
.assertThat()
87+
.statusCode(302)
88+
.header("location", containsString("/user-secured"))
89+
.cookie("laitnederc-sukrauq", notNullValue());
90+
91+
RestAssured
92+
.given()
93+
.filter(cookies)
94+
.redirects().follow(false)
95+
.when()
96+
.get("/jaxrs-secured/user-secured")
97+
.then()
98+
.assertThat()
99+
.statusCode(200)
100+
.body(equalTo("A secured message"));
101+
}
102+
103+
@Test
104+
void testBasicAuthentication() {
105+
RestAssured
106+
.given()
107+
.auth().preemptive().basic("user", "wrong-password")
108+
.get("/jaxrs-secured/user-secured")
109+
.then()
110+
.statusCode(401);
111+
RestAssured
112+
.given()
113+
.auth().preemptive().basic("user", "user")
114+
.get("/jaxrs-secured/user-secured")
115+
.then()
116+
.statusCode(200)
117+
.body(equalTo("A secured message"));
118+
}
119+
120+
public static class SecurityJpaConfiguration {
121+
122+
void configure(@Observes HttpSecurity httpSecurity) {
123+
var form = Form.builder()
124+
.loginPage("login")
125+
.errorPage("error")
126+
.landingPage("landing")
127+
.cookieName("laitnederc-sukrauq")
128+
.newCookieInterval(Duration.ofSeconds(5))
129+
.timeout(Duration.ofSeconds(5))
130+
.encryptionKey("CHANGEIT-CHANGEIT-CHANGEIT-CHANGEIT-CHANGEIT")
131+
.build();
132+
var jpa = SecurityJpa.jpa();
133+
httpSecurity.mechanism(form, jpa).basic(jpa);
134+
}
135+
136+
}
137+
138+
@ApplicationScoped
139+
public static class FailingIdentityProvider implements IdentityProvider<UsernamePasswordAuthenticationRequest> {
140+
141+
@Override
142+
public Class<UsernamePasswordAuthenticationRequest> getRequestType() {
143+
return UsernamePasswordAuthenticationRequest.class;
144+
}
145+
146+
@Override
147+
public Uni<SecurityIdentity> authenticate(UsernamePasswordAuthenticationRequest authenticationRequest,
148+
AuthenticationRequestContext authenticationRequestContext) {
149+
throw new IllegalStateException("This provider must never be invoked as we selected the JPA provider");
150+
}
151+
152+
@Override
153+
public int priority() {
154+
return Integer.MAX_VALUE;
155+
}
156+
}
157+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.quarkus.security.jpa.reactive.runtime;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collection;
5+
import java.util.Collections;
6+
7+
import jakarta.enterprise.inject.Instance;
8+
import jakarta.inject.Inject;
9+
10+
import io.quarkus.security.identity.IdentityProvider;
11+
import io.quarkus.security.jpa.SecurityJpa;
12+
13+
public final class SecurityJpaReactiveImpl implements SecurityJpa {
14+
15+
@Inject
16+
Instance<JpaReactiveIdentityProvider> jpaReactiveIdentityProvider;
17+
18+
@Inject
19+
Instance<JpaReactiveTrustedIdentityProvider> jpaReactiveTrustedIdentityProvider;
20+
21+
@Override
22+
public Collection<IdentityProvider<?>> getIdentityProviders() {
23+
final Collection<IdentityProvider<?>> result = new ArrayList<>();
24+
if (jpaReactiveIdentityProvider.isResolvable()) {
25+
result.add(jpaReactiveIdentityProvider.get());
26+
}
27+
if (jpaReactiveTrustedIdentityProvider.isResolvable()) {
28+
result.add(jpaReactiveTrustedIdentityProvider.get());
29+
}
30+
return Collections.unmodifiableCollection(result);
31+
}
32+
}

extensions/security-jpa/deployment/src/main/java/io/quarkus/security/jpa/deployment/QuarkusSecurityJpaProcessor.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.jboss.jandex.Index;
2727
import org.jboss.jandex.Type;
2828

29+
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
2930
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
3031
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
3132
import io.quarkus.arc.deployment.InjectionPointTransformerBuildItem;
@@ -55,6 +56,7 @@
5556
import io.quarkus.security.jpa.common.deployment.PanacheEntityPredicateBuildItem;
5657
import io.quarkus.security.jpa.runtime.JpaIdentityProvider;
5758
import io.quarkus.security.jpa.runtime.JpaTrustedIdentityProvider;
59+
import io.quarkus.security.jpa.runtime.SecurityJpaImpl;
5860

5961
class QuarkusSecurityJpaProcessor {
6062

@@ -118,6 +120,11 @@ PanacheEntityPredicateBuildItem panacheEntityPredicate(List<PanacheEntityClasses
118120
return new PanacheEntityPredicateBuildItem(collectPanacheEntities(panacheEntityClasses));
119121
}
120122

123+
@BuildStep
124+
AdditionalBeanBuildItem registerSecurityJpaImplCdiBean() {
125+
return AdditionalBeanBuildItem.unremovableOf(SecurityJpaImpl.class);
126+
}
127+
121128
private Set<String> collectPanacheEntities(List<PanacheEntityClassesBuildItem> panacheEntityClassesBuildItems) {
122129
Set<String> modelClasses = new HashSet<>();
123130
for (PanacheEntityClassesBuildItem panacheEntityClasses : panacheEntityClassesBuildItems) {

0 commit comments

Comments
 (0)