Allow CORS Access-Control-Allow-Headers customization (#43767)

Closes #12682

Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
Stian Thorgersen
2025-11-03 07:39:44 +01:00
committed by GitHub
parent 52ba359cc3
commit d0a7225b3d
5 changed files with 97 additions and 5 deletions

View File

@@ -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";

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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");
}
}
}