Skip to content

Commit 54bcf7a

Browse files
committed
feat(cors): add fluent api for programmatic set up
1 parent 135793b commit 54bcf7a

File tree

21 files changed

+778
-87
lines changed

21 files changed

+778
-87
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
@@ -596,6 +596,7 @@ The same authorization can be required with the `@PermissionsAllowed(value = { "
596596
* xref:security-openid-connect-multitenancy.adoc#programmatic-startup[Programmatic OIDC start-up for multitenant application]
597597
* xref:security-authentication-mechanisms.adoc#form-based-auth-programmatic-set-up[Set up Form-based authentication programmatically]
598598
* xref:security-authentication-mechanisms.adoc#mtls-programmatic-set-up[Set up the mutual TLS client authentication programmatically]
599+
* xref:security-cors.adoc#cors-filter-programmatic-set-up[Configuring the CORS filter programmatically]
599600

600601
[[standard-security-annotations]]
601602
== Authorization using annotations

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,61 @@ Allowing unrestricted origins in production environments poses severe security r
9191
For production, define explicit origins in the `quarkus.http.cors.origins` property.
9292
====
9393

94+
[[cors-filter-programmatic-set-up]]
95+
== Configuring the CORS filter programmatically
96+
97+
To enforce CORS policies in your application, enable the Quarkus CORS filter with the `io.quarkus.vertx.http.security.HttpSecurity` CDI event:
98+
99+
[source,java]
100+
----
101+
package org.acme.http.security;
102+
103+
import io.quarkus.vertx.http.security.HttpSecurity;
104+
import jakarta.enterprise.event.Observes;
105+
106+
public class CorsProgrammaticConfig {
107+
void configure(@Observes HttpSecurity httpSecurity) {
108+
httpSecurity.cors();
109+
}
110+
}
111+
----
112+
113+
As another example, here is how you can specify allowed origins:
114+
115+
[source,java]
116+
----
117+
package org.acme.http.security;
118+
119+
import io.quarkus.vertx.http.security.HttpSecurity;
120+
import jakarta.enterprise.event.Observes;
121+
122+
public class CorsProgrammaticConfig {
123+
void configure(@Observes HttpSecurity httpSecurity) {
124+
httpSecurity.cors("https://example.com", "https://example.io");
125+
}
126+
}
127+
----
128+
129+
The `io.quarkus.vertx.http.security.CORS` builder allows you to create a complete CORS configuration:
130+
131+
[source,java]
132+
----
133+
package org.acme.http.security;
134+
135+
import io.quarkus.vertx.http.security.CORS;
136+
import io.quarkus.vertx.http.security.HttpSecurity;
137+
import jakarta.enterprise.event.Observes;
138+
139+
public class CorsProgrammaticConfig {
140+
void configure(@Observes HttpSecurity httpSecurity) {
141+
httpSecurity.cors(CORS.builder()
142+
.origin("https://example.com")
143+
.method("POST")
144+
.build());
145+
}
146+
}
147+
----
148+
94149
== References
95150

96151
* xref:security-overview.adoc[Quarkus Security overview]

extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/DevUICORSFilter.java

Lines changed: 13 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
package io.quarkus.devui.runtime;
22

3-
import java.time.Duration;
4-
import java.util.ArrayList;
3+
import java.util.HashSet;
54
import java.util.List;
6-
import java.util.Optional;
5+
import java.util.Set;
76
import java.util.regex.Matcher;
87
import java.util.regex.Pattern;
98

@@ -12,6 +11,7 @@
1211

1312
import io.quarkus.vertx.http.runtime.cors.CORSConfig;
1413
import io.quarkus.vertx.http.runtime.cors.CORSFilter;
14+
import io.quarkus.vertx.http.security.CORS;
1515
import io.vertx.core.Handler;
1616
import io.vertx.core.http.HttpHeaders;
1717
import io.vertx.core.http.HttpServerRequest;
@@ -44,46 +44,16 @@ public DevUICORSFilter(List<String> hosts) {
4444
private static CORSFilter corsFilter(String allowedHost) {
4545
int httpPort = ConfigProvider.getConfig().getValue(HTTP_PORT_CONFIG_PROP, int.class);
4646
int httpsPort = ConfigProvider.getConfig().getValue(HTTPS_PORT_CONFIG_PROP, int.class);
47-
CORSConfig config = new CORSConfig() {
48-
@Override
49-
public Optional<List<String>> origins() {
50-
List<String> validOrigins = new ArrayList<>();
51-
validOrigins.add(HTTP_LOCAL_HOST + ":" + httpPort);
52-
validOrigins.add(HTTPS_LOCAL_HOST + ":" + httpsPort);
53-
validOrigins.add(HTTP_LOCAL_HOST_IP + ":" + httpPort);
54-
validOrigins.add(HTTPS_LOCAL_HOST_IP + ":" + httpsPort);
55-
56-
if (allowedHost != null) {
57-
validOrigins.add(allowedHost);
58-
}
59-
return Optional.of(validOrigins);
60-
}
61-
62-
@Override
63-
public Optional<List<String>> methods() {
64-
return Optional.empty();
65-
}
66-
67-
@Override
68-
public Optional<List<String>> headers() {
69-
return Optional.empty();
70-
}
71-
72-
@Override
73-
public Optional<List<String>> exposedHeaders() {
74-
return Optional.empty();
75-
}
76-
77-
@Override
78-
public Optional<Duration> accessControlMaxAge() {
79-
return Optional.empty();
80-
}
81-
82-
@Override
83-
public Optional<Boolean> accessControlAllowCredentials() {
84-
return Optional.empty();
85-
}
86-
};
47+
Set<String> validOrigins = new HashSet<>();
48+
validOrigins.add(HTTP_LOCAL_HOST + ":" + httpPort);
49+
validOrigins.add(HTTPS_LOCAL_HOST + ":" + httpsPort);
50+
validOrigins.add(HTTP_LOCAL_HOST_IP + ":" + httpPort);
51+
validOrigins.add(HTTPS_LOCAL_HOST_IP + ":" + httpsPort);
52+
53+
if (allowedHost != null) {
54+
validOrigins.add(allowedHost);
55+
}
56+
CORSConfig config = (CORSConfig) CORS.origins(validOrigins);
8757
return new CORSFilter(config);
8858
}
8959

extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiRecorder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public OpenApiRecorder(
2828
}
2929

3030
public Consumer<Route> corsFilter(Filter filter) {
31-
if (httpConfig.getValue().corsEnabled() && filter.getHandler() != null) {
31+
if (httpConfig.getValue().cors().enabled() && filter.getHandler() != null) {
3232
return new Consumer<Route>() {
3333
@Override
3434
public void accept(Route route) {

extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
4949
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
5050
import io.quarkus.arc.processor.BeanInfo;
51-
import io.quarkus.builder.item.EmptyBuildItem;
5251
import io.quarkus.builder.item.SimpleBuildItem;
5352
import io.quarkus.deployment.Capabilities;
5453
import io.quarkus.deployment.Capability;
@@ -80,6 +79,7 @@
8079
import io.quarkus.tls.deployment.spi.TlsRegistryBuildItem;
8180
import io.quarkus.vertx.core.deployment.IgnoredContextLocalDataKeysBuildItem;
8281
import io.quarkus.vertx.http.runtime.VertxHttpBuildTimeConfig;
82+
import io.quarkus.vertx.http.runtime.cors.CORSConfig;
8383
import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig;
8484
import io.quarkus.vertx.http.runtime.security.AuthorizationPolicyStorage;
8585
import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism;
@@ -294,17 +294,18 @@ void createHttpAuthenticationHandler(HttpSecurityRecorder recorder, Capabilities
294294
@Consume(TlsRegistryBuildItem.class) // we may need to register a TLS configuration for the mTLS
295295
@Consume(RuntimeConfigSetupCompleteBuildItem.class)
296296
@Produce(PreRouterFinalizationBuildItem.class)
297-
@Produce(HttpSecurityConfigSetupCompleteBuildItem.class)
298297
@Record(ExecutionTime.RUNTIME_INIT)
299298
@BuildStep
300-
void initializeHttpSecurity(Optional<HttpAuthenticationHandlerBuildItem> authenticationHandler,
299+
HttpSecurityConfigSetupCompleteBuildItem initializeHttpSecurity(
300+
Optional<HttpAuthenticationHandlerBuildItem> authenticationHandler,
301301
HttpSecurityRecorder recorder, BeanContainerBuildItem beanContainerBuildItem,
302302
ShutdownContextBuildItem shutdown) {
303303
if (authenticationHandler.isPresent()) {
304-
recorder.prepareHttpSecurityConfiguration(shutdown);
305-
recorder.initializeHttpAuthenticatorHandler(authenticationHandler.get().handler,
306-
beanContainerBuildItem.getValue());
304+
RuntimeValue<CORSConfig> programmaticCorsConfig = recorder.prepareHttpSecurityConfiguration(shutdown);
305+
recorder.initializeHttpAuthenticatorHandler(authenticationHandler.get().handler, beanContainerBuildItem.getValue());
306+
return new HttpSecurityConfigSetupCompleteBuildItem(programmaticCorsConfig);
307307
}
308+
return new HttpSecurityConfigSetupCompleteBuildItem(null);
308309
}
309310

310311
@BuildStep
@@ -897,7 +898,12 @@ public boolean getAsBoolean() {
897898
}
898899
}
899900

900-
static final class HttpSecurityConfigSetupCompleteBuildItem extends EmptyBuildItem {
901+
static final class HttpSecurityConfigSetupCompleteBuildItem extends SimpleBuildItem {
901902

903+
final RuntimeValue<CORSConfig> programmaticCorsConfig;
904+
905+
private HttpSecurityConfigSetupCompleteBuildItem(RuntimeValue<CORSConfig> programmaticCorsConfig) {
906+
this.programmaticCorsConfig = programmaticCorsConfig;
907+
}
902908
}
903909
}

extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import io.quarkus.vertx.core.deployment.CoreVertxBuildItem;
6262
import io.quarkus.vertx.core.deployment.EventLoopCountBuildItem;
6363
import io.quarkus.vertx.http.HttpServerOptionsCustomizer;
64+
import io.quarkus.vertx.http.deployment.HttpSecurityProcessor.HttpSecurityConfigSetupCompleteBuildItem;
6465
import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem;
6566
import io.quarkus.vertx.http.deployment.spi.FrameworkEndpointsBuildItem;
6667
import io.quarkus.vertx.http.deployment.spi.UseManagementInterfaceBuildItem;
@@ -72,6 +73,7 @@
7273
import io.quarkus.vertx.http.runtime.VertxHttpConfig.InsecureRequests;
7374
import io.quarkus.vertx.http.runtime.VertxHttpRecorder;
7475
import io.quarkus.vertx.http.runtime.attribute.ExchangeAttributeBuilder;
76+
import io.quarkus.vertx.http.runtime.cors.CORSConfig;
7577
import io.quarkus.vertx.http.runtime.cors.CORSRecorder;
7678
import io.quarkus.vertx.http.runtime.filters.Filter;
7779
import io.quarkus.vertx.http.runtime.filters.GracefulShutdownFilter;
@@ -148,8 +150,11 @@ FrameworkEndpointsBuildItem frameworkEndpoints(NonApplicationRootPathBuildItem n
148150

149151
@BuildStep
150152
@Record(ExecutionTime.RUNTIME_INIT)
151-
FilterBuildItem cors(CORSRecorder recorder) {
152-
return new FilterBuildItem(recorder.corsHandler(), SecurityHandlerPriorities.CORS);
153+
FilterBuildItem cors(CORSRecorder recorder,
154+
Optional<HttpSecurityConfigSetupCompleteBuildItem> httpSecurityConfigSetupCompleteBuildItem) {
155+
RuntimeValue<CORSConfig> programmaticCorsConfig = httpSecurityConfigSetupCompleteBuildItem
156+
.map(i -> i.programmaticCorsConfig).orElse(null);
157+
return new FilterBuildItem(recorder.corsHandler(programmaticCorsConfig), SecurityHandlerPriorities.CORS);
153158
}
154159

155160
@BuildStep
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package io.quarkus.vertx.http.cors;
2+
3+
import static io.restassured.RestAssured.given;
4+
import static org.hamcrest.Matchers.containsString;
5+
import static org.hamcrest.Matchers.not;
6+
7+
import java.time.Duration;
8+
import java.util.Set;
9+
10+
import jakarta.enterprise.event.Observes;
11+
12+
import org.junit.jupiter.api.DisplayName;
13+
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.api.extension.RegisterExtension;
15+
16+
import io.quarkus.test.QuarkusUnitTest;
17+
import io.quarkus.vertx.http.security.CORS;
18+
import io.quarkus.vertx.http.security.HttpSecurity;
19+
20+
public class CORSFluentApiFullConfigHandlerTest {
21+
22+
@RegisterExtension
23+
static QuarkusUnitTest runner = new QuarkusUnitTest()
24+
.withApplicationRoot((jar) -> jar
25+
.addClasses(BeanRegisteringRoute.class, CorsProgrammaticConfig.class));
26+
27+
@Test
28+
@DisplayName("Handles a detailed CORS config request correctly")
29+
public void corsFullConfigTestServlet() {
30+
given().header("Origin", "http://custom.origin.quarkus")
31+
.header("Access-Control-Request-Method", "GET")
32+
.header("Access-Control-Request-Headers", "X-Custom")
33+
.when()
34+
.options("/test").then()
35+
.statusCode(200)
36+
.header("Access-Control-Allow-Origin", "http://custom.origin.quarkus")
37+
.header("Access-Control-Allow-Methods", containsString("GET"))
38+
.header("Access-Control-Allow-Methods", containsString("PUT"))
39+
.header("Access-Control-Allow-Methods", containsString("POST"))
40+
.header("Access-Control-Allow-Methods", not(containsString("DELETE")))
41+
.header("Access-Control-Expose-Headers", "Content-Disposition")
42+
.header("Access-Control-Allow-Headers", "X-Custom")
43+
.header("Access-Control-Allow-Credentials", "false")
44+
.header("Access-Control-Max-Age", "86400");
45+
46+
given().header("Origin", "http://www.quarkus.io")
47+
.header("Access-Control-Request-Method", "PUT")
48+
.when()
49+
.options("/test").then()
50+
.statusCode(200)
51+
.header("Access-Control-Allow-Origin", "http://www.quarkus.io")
52+
.header("Access-Control-Allow-Methods", containsString("PUT"))
53+
.header("Access-Control-Allow-Methods", containsString("GET"))
54+
.header("Access-Control-Allow-Methods", containsString("POST"))
55+
.header("Access-Control-Allow-Methods", not(containsString("DELETE")))
56+
.header("Access-Control-Allow-Credentials", "false")
57+
.header("Access-Control-Expose-Headers", "Content-Disposition");
58+
}
59+
60+
@Test
61+
@DisplayName("Returns only allowed headers and methods")
62+
public void corsPartialMethodsTestServlet() {
63+
given().header("Origin", "http://custom.origin.quarkus")
64+
.header("Access-Control-Request-Method", "DELETE")
65+
.header("Access-Control-Request-Headers", "X-Custom, X-Custom2")
66+
.when()
67+
.options("/test").then()
68+
.statusCode(200)
69+
.header("Access-Control-Allow-Origin", "http://custom.origin.quarkus")
70+
.header("Access-Control-Allow-Methods", containsString("GET"))
71+
.header("Access-Control-Allow-Methods", containsString("PUT"))
72+
.header("Access-Control-Allow-Methods", containsString("POST"))
73+
.header("Access-Control-Allow-Methods", not(containsString("DELETE")))
74+
.header("Access-Control-Expose-Headers", "Content-Disposition")
75+
.header("Access-Control-Allow-Credentials", "false")
76+
.header("Access-Control-Allow-Headers", "X-Custom");// Should not return X-Custom2
77+
}
78+
79+
public static class CorsProgrammaticConfig {
80+
void configure(@Observes HttpSecurity httpSecurity) {
81+
httpSecurity.cors(CORS.builder()
82+
.origins(Set.of("http://custom.origin.quarkus", "http://www.quarkus.io"))
83+
.methods(Set.of("GET", "PUT", "POST"))
84+
.header("X-Custom")
85+
.exposedHeader("Content-Disposition")
86+
.accessControlMaxAge(Duration.ofDays(1))
87+
.accessControlAllowCredentials(false)
88+
.build());
89+
}
90+
}
91+
}

0 commit comments

Comments
 (0)