diff --git a/server-spi-private/src/main/java/org/keycloak/services/cors/Cors.java b/server-spi-private/src/main/java/org/keycloak/services/cors/Cors.java index 71bb375cac8..1714be41d83 100755 --- a/server-spi-private/src/main/java/org/keycloak/services/cors/Cors.java +++ b/server-spi-private/src/main/java/org/keycloak/services/cors/Cors.java @@ -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 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"; diff --git a/services/src/main/java/org/keycloak/services/cors/DefaultCors.java b/services/src/main/java/org/keycloak/services/cors/DefaultCors.java index d41a2edd4d6..91863e04ed0 100755 --- a/services/src/main/java/org/keycloak/services/cors/DefaultCors.java +++ b/services/src/main/java/org/keycloak/services/cors/DefaultCors.java @@ -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 allowedOrigins; private Set allowedMethods; private Set 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); } } diff --git a/services/src/main/java/org/keycloak/services/cors/DefaultCorsFactory.java b/services/src/main/java/org/keycloak/services/cors/DefaultCorsFactory.java index 17877512aa3..40904c2c340 100644 --- a/services/src/main/java/org/keycloak/services/cors/DefaultCorsFactory.java +++ b/services/src/main/java/org/keycloak/services/cors/DefaultCorsFactory.java @@ -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 Dmitry Telegin @@ -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 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 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(); + } } diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServerConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServerConfigBuilder.java index b0564be7f2b..7fc8799ffe2 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServerConfigBuilder.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServerConfigBuilder.java @@ -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 options = new HashMap<>(); private final Set 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; diff --git a/tests/base/src/test/java/org/keycloak/tests/cors/CustomCorsAllowedHeadersTest.java b/tests/base/src/test/java/org/keycloak/tests/cors/CustomCorsAllowedHeadersTest.java new file mode 100644 index 00000000000..ef5a525420d --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/cors/CustomCorsAllowedHeadersTest.java @@ -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 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"); + } + } + +}