diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java index 472afb7d45..7f86ba1bde 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java @@ -101,6 +101,7 @@ public class AuthenticatedActionsHandler { if (!deployment.isCors()) return false; KeycloakSecurityContext securityContext = facade.getSecurityContext(); String origin = facade.getRequest().getHeader(CorsHeaders.ORIGIN); + String exposeHeaders = deployment.getCorsExposedHeaders(); String requestOrigin = UriUtils.getOrigin(facade.getRequest().getURI()); log.debugv("Origin: {0} uri: {1}", origin, facade.getRequest().getURI()); if (securityContext != null && origin != null && !origin.equals(requestOrigin)) { @@ -124,6 +125,9 @@ public class AuthenticatedActionsHandler { facade.getResponse().setStatus(200); facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin); facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); + if (exposeHeaders != null) { + facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, exposeHeaders); + } } else { log.debugv("cors validation not needed as we're not a secure session or origin header was null: {0}", facade.getRequest().getURI()); } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CorsHeaders.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CorsHeaders.java index 715af3dce6..416c3923f8 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CorsHeaders.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CorsHeaders.java @@ -30,4 +30,5 @@ public interface CorsHeaders { String ORIGIN = "Origin"; String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; + String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java index ba7bc5d759..31f842c654 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java @@ -73,6 +73,7 @@ public class KeycloakDeployment { protected int corsMaxAge = -1; protected String corsAllowedHeaders; protected String corsAllowedMethods; + protected String corsExposedHeaders; protected boolean exposeToken; protected boolean alwaysRefreshToken; protected boolean registerNodeAtStartup; @@ -325,6 +326,14 @@ public class KeycloakDeployment { this.corsAllowedMethods = corsAllowedMethods; } + public String getCorsExposedHeaders() { + return corsExposedHeaders; + } + + public void setCorsExposedHeaders(String corsExposedHeaders) { + this.corsExposedHeaders = corsExposedHeaders; + } + public boolean isExposeToken() { return exposeToken; } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java index 2fd92760c6..a65175390e 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java @@ -96,6 +96,7 @@ public class KeycloakDeploymentBuilder { deployment.setCorsMaxAge(adapterConfig.getCorsMaxAge()); deployment.setCorsAllowedHeaders(adapterConfig.getCorsAllowedHeaders()); deployment.setCorsAllowedMethods(adapterConfig.getCorsAllowedMethods()); + deployment.setCorsExposedHeaders(adapterConfig.getCorsExposedHeaders()); } // https://tools.ietf.org/html/rfc7636 diff --git a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java index 233c1ed2f0..a4f04ec152 100644 --- a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java +++ b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java @@ -53,6 +53,7 @@ public class KeycloakDeploymentBuilderTest { assertEquals(1000, deployment.getCorsMaxAge()); assertEquals("POST, PUT, DELETE, GET", deployment.getCorsAllowedMethods()); assertEquals("X-Custom, X-Custom2", deployment.getCorsAllowedHeaders()); + assertEquals("X-Custom3, X-Custom4", deployment.getCorsExposedHeaders()); assertTrue(deployment.isBearerOnly()); assertTrue(deployment.isPublicClient()); assertTrue(deployment.isEnableBasicAuth()); diff --git a/adapters/oidc/adapter-core/src/test/resources/keycloak.json b/adapters/oidc/adapter-core/src/test/resources/keycloak.json index 9f0a204826..f53432ffee 100644 --- a/adapters/oidc/adapter-core/src/test/resources/keycloak.json +++ b/adapters/oidc/adapter-core/src/test/resources/keycloak.json @@ -9,6 +9,7 @@ "cors-max-age": 1000, "cors-allowed-methods": "POST, PUT, DELETE, GET", "cors-allowed-headers": "X-Custom, X-Custom2", + "cors-exposed-headers": "X-Custom3, X-Custom4", "bearer-only": true, "public-client": true, "enable-basic-auth": true, diff --git a/adapters/oidc/wildfly/wf8-subsystem/src/main/java/org/keycloak/subsystem/wf8/extension/SharedAttributeDefinitons.java b/adapters/oidc/wildfly/wf8-subsystem/src/main/java/org/keycloak/subsystem/wf8/extension/SharedAttributeDefinitons.java index e4ff98816a..a7676eade0 100755 --- a/adapters/oidc/wildfly/wf8-subsystem/src/main/java/org/keycloak/subsystem/wf8/extension/SharedAttributeDefinitons.java +++ b/adapters/oidc/wildfly/wf8-subsystem/src/main/java/org/keycloak/subsystem/wf8/extension/SharedAttributeDefinitons.java @@ -124,6 +124,12 @@ public class SharedAttributeDefinitons { .setAllowExpression(true) .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) .build(); + protected static final SimpleAttributeDefinition CORS_EXPOSED_HEADERS = + new SimpleAttributeDefinitionBuilder("cors-exposed-headers", ModelType.STRING, true) + .setXmlName("cors-exposed-headers") + .setAllowExpression(true) + .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) + .build(); protected static final SimpleAttributeDefinition EXPOSE_TOKEN = new SimpleAttributeDefinitionBuilder("expose-token", ModelType.BOOLEAN, true) .setXmlName("expose-token") @@ -191,6 +197,7 @@ public class SharedAttributeDefinitons { ATTRIBUTES.add(CORS_MAX_AGE); ATTRIBUTES.add(CORS_ALLOWED_HEADERS); ATTRIBUTES.add(CORS_ALLOWED_METHODS); + ATTRIBUTES.add(CORS_EXPOSED_HEADERS); ATTRIBUTES.add(EXPOSE_TOKEN); ATTRIBUTES.add(AUTH_SERVER_URL_FOR_BACKEND_REQUESTS); ATTRIBUTES.add(ALWAYS_REFRESH_TOKEN); diff --git a/adapters/oidc/wildfly/wf8-subsystem/src/main/resources/org/keycloak/subsystem/wf8/extension/LocalDescriptions.properties b/adapters/oidc/wildfly/wf8-subsystem/src/main/resources/org/keycloak/subsystem/wf8/extension/LocalDescriptions.properties index 6244b0c797..71101a1c24 100755 --- a/adapters/oidc/wildfly/wf8-subsystem/src/main/resources/org/keycloak/subsystem/wf8/extension/LocalDescriptions.properties +++ b/adapters/oidc/wildfly/wf8-subsystem/src/main/resources/org/keycloak/subsystem/wf8/extension/LocalDescriptions.properties @@ -39,6 +39,7 @@ keycloak.realm.client-key-password=n/a keycloak.realm.cors-max-age=CORS max-age header keycloak.realm.cors-allowed-headers=CORS allowed headers keycloak.realm.cors-allowed-methods=CORS allowed methods +keycloak.realm.cors-exposed-headers=CORS exposed headers keycloak.realm.expose-token=Enable secure URL that exposes access token keycloak.realm.auth-server-url-for-backend-requests=URL to use to make background calls to auth server keycloak.realm.always-refresh-token=Refresh token on every single web request @@ -73,6 +74,7 @@ keycloak.secure-deployment.client-key-password=n/a keycloak.secure-deployment.cors-max-age=CORS max-age header keycloak.secure-deployment.cors-allowed-headers=CORS allowed headers keycloak.secure-deployment.cors-allowed-methods=CORS allowed methods +keycloak.secure-deployment.cors-exposed-headers=CORS exposed headers keycloak.secure-deployment.expose-token=Enable secure URL that exposes access token keycloak.secure-deployment.auth-server-url-for-backend-requests=URL to use to make background calls to auth server keycloak.secure-deployment.always-refresh-token=Refresh token on every single web request diff --git a/adapters/oidc/wildfly/wf8-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd b/adapters/oidc/wildfly/wf8-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd index e9839bc069..cc51ec41ab 100755 --- a/adapters/oidc/wildfly/wf8-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd +++ b/adapters/oidc/wildfly/wf8-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd @@ -58,6 +58,7 @@ + @@ -88,6 +89,7 @@ + diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SharedAttributeDefinitons.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SharedAttributeDefinitons.java index 02d14e5b81..fafed423c9 100755 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SharedAttributeDefinitons.java +++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SharedAttributeDefinitons.java @@ -124,6 +124,12 @@ public class SharedAttributeDefinitons { .setAllowExpression(true) .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) .build(); + protected static final SimpleAttributeDefinition CORS_EXPOSED_HEADERS = + new SimpleAttributeDefinitionBuilder("cors-exposed-headers", ModelType.STRING, true) + .setXmlName("cors-exposed-headers") + .setAllowExpression(true) + .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) + .build(); protected static final SimpleAttributeDefinition EXPOSE_TOKEN = new SimpleAttributeDefinitionBuilder("expose-token", ModelType.BOOLEAN, true) .setXmlName("expose-token") @@ -175,6 +181,8 @@ public class SharedAttributeDefinitons { + + protected static final List ATTRIBUTES = new ArrayList(); static { ATTRIBUTES.add(REALM_PUBLIC_KEY); @@ -192,6 +200,7 @@ public class SharedAttributeDefinitons { ATTRIBUTES.add(CORS_MAX_AGE); ATTRIBUTES.add(CORS_ALLOWED_HEADERS); ATTRIBUTES.add(CORS_ALLOWED_METHODS); + ATTRIBUTES.add(CORS_EXPOSED_HEADERS); ATTRIBUTES.add(EXPOSE_TOKEN); ATTRIBUTES.add(AUTH_SERVER_URL_FOR_BACKEND_REQUESTS); ATTRIBUTES.add(ALWAYS_REFRESH_TOKEN); diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties index c0ca52e0dc..a297c1dcc3 100755 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties +++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties @@ -39,6 +39,7 @@ keycloak.realm.client-key-password=n/a keycloak.realm.cors-max-age=CORS max-age header keycloak.realm.cors-allowed-headers=CORS allowed headers keycloak.realm.cors-allowed-methods=CORS allowed methods +keycloak.realm.cors-exposed-headers=CORS exposed headers keycloak.realm.expose-token=Enable secure URL that exposes access token keycloak.realm.auth-server-url-for-backend-requests=URL to use to make background calls to auth server keycloak.realm.always-refresh-token=Refresh token on every single web request @@ -74,6 +75,7 @@ keycloak.secure-deployment.client-key-password=n/a keycloak.secure-deployment.cors-max-age=CORS max-age header keycloak.secure-deployment.cors-allowed-headers=CORS allowed headers keycloak.secure-deployment.cors-allowed-methods=CORS allowed methods +keycloak.secure-deployment.cors-exposed-headers=CORS exposed headers keycloak.secure-deployment.expose-token=Enable secure URL that exposes access token keycloak.secure-deployment.auth-server-url-for-backend-requests=URL to use to make background calls to auth server keycloak.secure-deployment.always-refresh-token=Refresh token on every single web request diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd index 84399a361a..81182090d9 100755 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd +++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd @@ -58,6 +58,7 @@ + @@ -88,6 +89,7 @@ + diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java index f063962a55..ddd525bbd7 100755 --- a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java +++ b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java @@ -29,7 +29,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder; @JsonPropertyOrder({"realm", "realm-public-key", "auth-server-url", "ssl-required", "resource", "public-client", "credentials", "use-resource-role-mappings", - "enable-cors", "cors-max-age", "cors-allowed-methods", + "enable-cors", "cors-max-age", "cors-allowed-methods", "cors-exposed-headers", "expose-token", "bearer-only", "autodetect-bearer-only", "connection-pool-size", "allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password", diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java index dfe4ae633d..4a2b7e288f 100755 --- a/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java +++ b/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java @@ -33,7 +33,7 @@ import java.util.TreeMap; @JsonPropertyOrder({"realm", "realm-public-key", "auth-server-url", "ssl-required", "resource", "public-client", "credentials", "use-resource-role-mappings", - "enable-cors", "cors-max-age", "cors-allowed-methods", + "enable-cors", "cors-max-age", "cors-allowed-methods", "cors-exposed-headers", "expose-token", "bearer-only", "autodetect-bearer-only", "enable-basic-auth"}) public class BaseAdapterConfig extends BaseRealmConfig { @JsonProperty("resource") @@ -48,6 +48,8 @@ public class BaseAdapterConfig extends BaseRealmConfig { protected String corsAllowedHeaders; @JsonProperty("cors-allowed-methods") protected String corsAllowedMethods; + @JsonProperty("cors-exposed-headers") + protected String corsExposedHeaders; @JsonProperty("expose-token") protected boolean exposeToken; @JsonProperty("bearer-only") @@ -110,6 +112,14 @@ public class BaseAdapterConfig extends BaseRealmConfig { this.corsAllowedMethods = corsAllowedMethods; } + public String getCorsExposedHeaders() { + return corsExposedHeaders; + } + + public void setCorsExposedHeaders(String corsExposedHeaders) { + this.corsExposedHeaders = corsExposedHeaders; + } + public boolean isExposeToken() { return exposeToken; }