mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-21 06:20:05 -06:00
Allow CORS Access-Control-Allow-Headers customization (#43767)
Closes #12682 Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
package org.keycloak.services.cors;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
@@ -36,7 +37,14 @@ public interface Cors extends Provider {
|
||||
|
||||
long DEFAULT_MAX_AGE = TimeUnit.HOURS.toSeconds(1);
|
||||
String DEFAULT_ALLOW_METHODS = "GET, HEAD, OPTIONS";
|
||||
String DEFAULT_ALLOW_HEADERS = "Origin, Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers, DPoP";
|
||||
Set<String> DEFAULT_ALLOW_HEADERS = Set.of(
|
||||
"Origin",
|
||||
"Accept",
|
||||
"X-Requested-With",
|
||||
"Content-Type",
|
||||
"Access-Control-Request-Method",
|
||||
"Access-Control-Request-Headers",
|
||||
"DPoP");
|
||||
|
||||
String ORIGIN_HEADER = "Origin";
|
||||
String AUTHORIZATION_HEADER = "Authorization";
|
||||
|
||||
@@ -47,6 +47,7 @@ public class DefaultCors implements Cors {
|
||||
private final HttpResponse response;
|
||||
private final KeycloakSession session;
|
||||
private ResponseBuilder builder;
|
||||
private final String allowedHeaders;
|
||||
private Set<String> allowedOrigins;
|
||||
private Set<String> allowedMethods;
|
||||
private Set<String> exposedHeaders;
|
||||
@@ -55,10 +56,11 @@ public class DefaultCors implements Cors {
|
||||
private boolean auth;
|
||||
private boolean failOnInvalidOrigin;
|
||||
|
||||
DefaultCors(KeycloakSession session) {
|
||||
DefaultCors(KeycloakSession session, String allowedHeaders) {
|
||||
this.session = session;
|
||||
this.request = session.getContext().getHttpRequest();
|
||||
this.response = session.getContext().getHttpResponse();
|
||||
this.allowedHeaders = allowedHeaders;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -197,9 +199,9 @@ public class DefaultCors implements Cors {
|
||||
|
||||
if (preflight) {
|
||||
if (auth) {
|
||||
response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS, String.format("%s, %s", DEFAULT_ALLOW_HEADERS, AUTHORIZATION_HEADER));
|
||||
response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS, String.format("%s, %s", allowedHeaders, AUTHORIZATION_HEADER));
|
||||
} else {
|
||||
response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS, DEFAULT_ALLOW_HEADERS);
|
||||
response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS, allowedHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,13 @@ package org.keycloak.services.cors;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:demetrio@carretti.pro">Dmitry Telegin</a>
|
||||
@@ -27,14 +34,23 @@ import org.keycloak.models.KeycloakSessionFactory;
|
||||
public class DefaultCorsFactory implements CorsFactory {
|
||||
|
||||
private static final String PROVIDER_ID = "default";
|
||||
private String allowedHeaders;
|
||||
|
||||
@Override
|
||||
public Cors create(KeycloakSession session) {
|
||||
return new DefaultCors(session);
|
||||
return new DefaultCors(session, allowedHeaders);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
Set<String> allowedHeaders = new HashSet<>(Cors.DEFAULT_ALLOW_HEADERS);
|
||||
|
||||
String[] customAllowedHeaders = config.getArray("allowedHeaders");
|
||||
if (customAllowedHeaders != null) {
|
||||
allowedHeaders.addAll(Arrays.asList(customAllowedHeaders));
|
||||
}
|
||||
|
||||
this.allowedHeaders = String.join(", ", allowedHeaders);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -50,4 +66,15 @@ public class DefaultCorsFactory implements CorsFactory {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigMetadata() {
|
||||
return ProviderConfigurationBuilder.create()
|
||||
.property()
|
||||
.name("allowedHeaders")
|
||||
.type("string")
|
||||
.helpText("A comma-separated list of additional allowed headers for CORS requests")
|
||||
.defaultValue(false)
|
||||
.add()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import java.util.stream.Collectors;
|
||||
|
||||
public class KeycloakServerConfigBuilder {
|
||||
|
||||
private static final String SPI_OPTION = "spi-%s--%s--%s";
|
||||
|
||||
private final String command;
|
||||
private final Map<String, String> options = new HashMap<>();
|
||||
private final Set<String> features = new HashSet<>();
|
||||
@@ -95,6 +97,11 @@ public class KeycloakServerConfigBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public KeycloakServerConfigBuilder spiOption(String spi, String provider, String key, String value) {
|
||||
options.put(String.format(SPI_OPTION, spi, provider, key), value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public KeycloakServerConfigBuilder dependency(String groupId, String artifactId) {
|
||||
dependencies.add(new DependencyBuilder().setGroupId(groupId).setArtifactId(artifactId).build());
|
||||
return this;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.keycloak.tests.cors;
|
||||
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.http.simple.SimpleHttp;
|
||||
import org.keycloak.http.simple.SimpleHttpResponse;
|
||||
import org.keycloak.services.cors.Cors;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.InjectSimpleHttp;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.server.KeycloakServerConfig;
|
||||
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@KeycloakIntegrationTest(config = CustomCorsAllowedHeadersTest.CustomCorsAllowedHeadersServerConfig.class)
|
||||
public class CustomCorsAllowedHeadersTest {
|
||||
|
||||
@InjectRealm
|
||||
ManagedRealm realm;
|
||||
|
||||
@InjectSimpleHttp
|
||||
SimpleHttp simpleHttp;
|
||||
|
||||
@Test
|
||||
public void testCustomAllowedHeaders() throws IOException {
|
||||
List<String> list;
|
||||
try (SimpleHttpResponse response = simpleHttp.doOptions(realm.getBaseUrl() + "/.well-known/openid-configuration").header("Origin", "https://something").asResponse()) {
|
||||
Assertions.assertEquals(200, response.getStatus());
|
||||
list = Arrays.stream(response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_HEADERS).split(", ")).map(String::trim).toList();
|
||||
}
|
||||
MatcherAssert.assertThat(list, Matchers.hasItems("uber-trace-id", "x-b3-traceid"));
|
||||
}
|
||||
|
||||
public static class CustomCorsAllowedHeadersServerConfig implements KeycloakServerConfig {
|
||||
|
||||
@Override
|
||||
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
|
||||
return config.spiOption("cors", "default", "allowed-headers", "uber-trace-id,x-b3-traceid");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user