From a65508ca13da5ad41ae0f4aa045ae0dd6219f3dc Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Mon, 8 Apr 2024 18:36:10 -0300 Subject: [PATCH] Simplifying the CORS SPI and the default implementation Closes #27646 Signed-off-by: Pedro Igor --- .../java/org/keycloak/services/cors/Cors.java | 106 +++++++++--------- .../AuthorizationTokenService.java | 5 +- .../oidc/DefaultTokenExchangeProvider.java | 6 +- .../oidc/OIDCLoginProtocolService.java | 6 +- .../oidc/endpoints/LogoutEndpoint.java | 20 ++-- .../oidc/endpoints/TokenEndpoint.java | 4 +- .../endpoints/TokenRevocationEndpoint.java | 6 +- .../oidc/endpoints/UserInfoEndpoint.java | 6 +- .../grants/ClientCredentialsGrantType.java | 2 +- .../oidc/grants/OAuth2GrantTypeBase.java | 2 +- .../grants/PreAuthorizedCodeGrantType.java | 2 +- .../oidc/grants/RefreshTokenGrantType.java | 2 +- ...urceOwnerPasswordCredentialsGrantType.java | 5 +- .../device/endpoints/DeviceEndpoint.java | 6 +- .../oidc/par/endpoints/ParEndpoint.java | 9 +- .../oidc/utils/AuthorizeClientUtil.java | 4 +- .../services/CorsErrorResponseException.java | 2 +- .../keycloak/services/cors/DefaultCors.java | 78 ++++--------- .../services/cors/DefaultCorsFactory.java | 2 +- .../resources/IdentityBrokerService.java | 4 +- .../services/resources/JsResource.java | 6 +- .../resources/PublicRealmResource.java | 4 +- .../services/resources/RealmsResource.java | 6 +- .../services/resources/ThemeResource.java | 5 +- .../resources/account/AccountLoader.java | 6 +- .../account/CorsPreflightService.java | 11 +- .../account/LinkedAccountsResource.java | 6 +- .../resources/admin/AdminConsole.java | 11 +- .../admin/AdminCorsPreflightService.java | 9 +- .../services/resources/admin/AdminRoot.java | 14 +-- .../admin/RealmsAdminResourcePreflight.java | 2 +- .../java/org/keycloak/utils/OAuth2Error.java | 4 +- .../account/AccountRestServiceTest.java | 26 +++++ 33 files changed, 182 insertions(+), 205 deletions(-) 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 424bd67e12..7acb0e9ade 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,13 +18,11 @@ package org.keycloak.services.cors; import java.util.concurrent.TimeUnit; -import java.util.function.BiConsumer; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.ResponseBuilder; -import org.keycloak.http.HttpRequest; -import org.keycloak.http.HttpResponse; +import org.keycloak.common.util.Resteasy; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.provider.Provider; @@ -36,59 +34,67 @@ import org.keycloak.utils.KeycloakSessionUtil; */ public interface Cors extends Provider { - public static final long DEFAULT_MAX_AGE = TimeUnit.HOURS.toSeconds(1); - public static final String DEFAULT_ALLOW_METHODS = "GET, HEAD, OPTIONS"; - public static final String DEFAULT_ALLOW_HEADERS = "Origin, Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers, DPoP"; + 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"; - public static final String ORIGIN_HEADER = "Origin"; - public static final String AUTHORIZATION_HEADER = "Authorization"; + String ORIGIN_HEADER = "Origin"; + String AUTHORIZATION_HEADER = "Authorization"; - public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; - public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; - public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; - public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; - public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; - public static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age"; + String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; + String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; + String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; + String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; + String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; + String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age"; - public static final String ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD = "*"; - public static final String INCLUDE_REDIRECTS = "+"; + String ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD = "*"; - public static Cors add(HttpRequest request, ResponseBuilder response) { + static Cors builder() { KeycloakSession session = KeycloakSessionUtil.getKeycloakSession(); - return session.getProvider(Cors.class).request(request).builder(response); + return session.getProvider(Cors.class); } - public static Cors add(HttpRequest request) { - KeycloakSession session = KeycloakSessionUtil.getKeycloakSession(); - return session.getProvider(Cors.class).request(request); + Cors builder(ResponseBuilder builder); + + Cors preflight(); + + Cors auth(); + + Cors allowAllOrigins(); + + Cors allowedOrigins(KeycloakSession session, ClientModel client); + + Cors allowedOrigins(AccessToken token); + + Cors allowedOrigins(String... allowedOrigins); + + Cors allowedMethods(String... allowedMethods); + + Cors exposedHeaders(String... exposedHeaders); + + /** + * Add the CORS headers to the current {@link org.keycloak.http.HttpResponse}. + */ + void add(); + + /** + *

Add the CORS headers to the current server {@link org.keycloak.http.HttpResponse} and returns a {@link Response} based + * on the given {@code builder}. + * + *

This is a convenient method to make it easier to return a {@link Response} from methods while at the same time + * adding the corresponding CORS headers to the underlying server response. + * + * @param builder the response builder + * @return the response built from the response builder + */ + default Response add(ResponseBuilder builder) { + if (builder == null) { + throw new IllegalStateException("builder is not set"); + } + + add(); + + return builder.build(); } - - public Cors request(HttpRequest request); - - public Cors builder(ResponseBuilder builder); - - public Cors preflight(); - - public Cors auth(); - - public Cors allowAllOrigins(); - - public Cors allowedOrigins(KeycloakSession session, ClientModel client); - - public Cors allowedOrigins(AccessToken token); - - public Cors allowedOrigins(String... allowedOrigins); - - public Cors allowedMethods(String... allowedMethods); - - public Cors exposedHeaders(String... exposedHeaders); - - public Cors addExposedHeaders(String... exposedHeaders); - - public Response build(); - - public boolean build(HttpResponse response); - - public boolean build(BiConsumer addHeader); - } diff --git a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java index 7f914533ac..e4a92a97a0 100644 --- a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java +++ b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java @@ -273,10 +273,11 @@ public class AuthorizationTokenService { } private Response createSuccessfulResponse(Object response, KeycloakAuthorizationRequest request) { - return Cors.add(request.getHttpRequest(), Response.status(Status.OK).type(MediaType.APPLICATION_JSON_TYPE).entity(response)) + return Cors.builder() .allowedOrigins(request.getKeycloakSession(), request.getKeycloakSession().getContext().getClient()) .allowedMethods(HttpMethod.POST) - .exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); + .exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS) + .add(Response.status(Status.OK).type(MediaType.APPLICATION_JSON_TYPE).entity(response)); } private boolean isPublicClientRequestingEntitlementWithClaims(KeycloakAuthorizationRequest request) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/DefaultTokenExchangeProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/DefaultTokenExchangeProvider.java index 1ee995bef1..cf78f0c281 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/DefaultTokenExchangeProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/DefaultTokenExchangeProvider.java @@ -279,7 +279,7 @@ public class DefaultTokenExchangeProvider implements TokenExchangeProvider { throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN); } Response response = ((ExchangeTokenToIdentityProviderToken)provider).exchangeFromToken(session.getContext().getUri(), event, client, targetUserSession, targetUser, formParams); - return cors.builder(Response.fromResponse(response)).build(); + return cors.add(Response.fromResponse(response)); } @@ -451,7 +451,7 @@ public class DefaultTokenExchangeProvider implements TokenExchangeProvider { event.success(); - return cors.builder(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).build(); + return cors.add(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)); } protected Response exchangeClientToSAML2Client(UserModel targetUser, UserSessionModel targetUserSession, String requestedTokenType, ClientModel targetClient) { @@ -501,7 +501,7 @@ public class DefaultTokenExchangeProvider implements TokenExchangeProvider { event.success(); - return cors.builder(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).build(); + return cors.add(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)); } protected Response exchangeExternalToken(String issuer, String subjectToken) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java index 4862526566..0d7eb735d4 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java @@ -199,7 +199,7 @@ public class OIDCLoginProtocolService { @Path("certs") @Produces(MediaType.APPLICATION_JSON) public Response getVersionPreflight() { - return Cors.add(request, Response.ok()).allowedMethods("GET").preflight().auth().build(); + return Cors.builder().allowedMethods("GET").preflight().auth().add(Response.ok()); } @GET @@ -232,7 +232,7 @@ public class OIDCLoginProtocolService { keySet.setKeys(jwks); Response.ResponseBuilder responseBuilder = Response.ok(keySet).cacheControl(CacheControlUtil.getDefaultCacheControl()); - return Cors.add(request, responseBuilder).allowedOrigins("*").auth().build(); + return Cors.builder().allowedOrigins("*").auth().add(responseBuilder); } @Path("userinfo") @@ -276,7 +276,7 @@ public class OIDCLoginProtocolService { private void checkSsl() { if (!session.getContext().getUri().getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) { - Cors cors = Cors.add(request).auth().allowedMethods(request.getHttpMethod()).auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS); + Cors cors = Cors.builder().auth().allowedMethods(request.getHttpMethod()).auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS); throw new CorsErrorResponseException(cors.allowAllOrigins(), OAuthErrorException.INVALID_REQUEST, "HTTPS required", Response.Status.FORBIDDEN); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java index 6323205491..a6c1e32885 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java @@ -130,7 +130,7 @@ public class LogoutEndpoint { @Path("/") @OPTIONS public Response issueUserInfoPreflight() { - return Cors.add(this.request, Response.ok()).auth().preflight().build(); + return Cors.builder().auth().preflight().add(Response.ok()); } /** @@ -496,7 +496,7 @@ public class LogoutEndpoint { * @return */ private Response logoutToken() { - cors = Cors.add(request).auth().allowedMethods("POST").auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS); + cors = Cors.builder().auth().allowedMethods("POST").auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS); MultivaluedMap form = request.getDecodedFormParameters(); checkSsl(); @@ -550,7 +550,7 @@ public class LogoutEndpoint { } } - return cors.builder(Response.noContent()).build(); + return cors.add(Response.noContent()); } /** @@ -618,18 +618,16 @@ public class LogoutEndpoint { session.getProvider(SecurityHeadersProvider.class).options().allowEmptyContentType(); if (oneOrMoreDownstreamLogoutsFailed(backchannelLogoutResponse)) { - return Cors.add(request) + return Cors.builder() .auth() - .builder(Response.status(Response.Status.GATEWAY_TIMEOUT) - .type(MediaType.APPLICATION_JSON_TYPE)) - .build(); + .add(Response.status(Response.Status.GATEWAY_TIMEOUT) + .type(MediaType.APPLICATION_JSON_TYPE)); } - return Cors.add(request) + return Cors.builder() .auth() - .builder(Response.ok() - .type(MediaType.APPLICATION_JSON_TYPE)) - .build(); + .add(Response.ok() + .type(MediaType.APPLICATION_JSON_TYPE)); } private BackchannelLogoutResponse backchannelLogoutWithSessionId(String sessionId, diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index 2a36487b27..5c8268e592 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -108,7 +108,7 @@ public class TokenEndpoint { @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @POST public Response processGrantRequest() { - cors = Cors.add(request).auth().allowedMethods("POST").auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS); + cors = Cors.builder().auth().allowedMethods("POST").auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS); MultivaluedMap formParameters = request.getDecodedFormParameters(); @@ -150,7 +150,7 @@ public class TokenEndpoint { if (logger.isDebugEnabled()) { logger.debugv("CORS preflight from: {0}", headers.getRequestHeaders().getFirst("Origin")); } - return Cors.add(request, Response.ok()).auth().preflight().allowedMethods("POST", "OPTIONS").build(); + return Cors.builder().auth().preflight().allowedMethods("POST", "OPTIONS").add(Response.ok()); } private void checkSsl() { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java index 4d4dae73e0..ee8c322f07 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java @@ -91,7 +91,7 @@ public class TokenRevocationEndpoint { public Response revoke() { event.event(EventType.REVOKE_GRANT); - cors = Cors.add(request).auth().allowedMethods("POST").auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS); + cors = Cors.builder().auth().allowedMethods("POST").auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS); checkSsl(); checkRealm(); @@ -130,12 +130,12 @@ public class TokenRevocationEndpoint { } session.getProvider(SecurityHeadersProvider.class).options().allowEmptyContentType(); - return cors.builder(Response.ok()).build(); + return cors.add(Response.ok()); } @OPTIONS public Response preflight() { - return Cors.add(request, Response.ok()).auth().preflight().allowedMethods("POST", "OPTIONS").build(); + return Cors.builder().auth().preflight().allowedMethods("POST", "OPTIONS").add(Response.ok()); } private void checkSsl() { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java index 332eb57605..fafdbe4f48 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java @@ -112,7 +112,7 @@ public class UserInfoEndpoint { @Path("/") @OPTIONS public Response issueUserInfoPreflight() { - return Cors.add(this.request, Response.ok()).auth().preflight().build(); + return Cors.builder().auth().preflight().add(Response.ok()); } @Path("/") @@ -322,7 +322,7 @@ public class UserInfoEndpoint { event.success(); - return cors.builder(responseBuilder).build(); + return cors.add(responseBuilder); } private String jweFromContent(String content, String jweContentType) { @@ -363,7 +363,7 @@ public class UserInfoEndpoint { } private void setupCors() { - cors = Cors.add(request).auth().allowedMethods(request.getHttpMethod()).auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS); + cors = Cors.builder().auth().allowedMethods(request.getHttpMethod()).auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS); error.cors(cors); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ClientCredentialsGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ClientCredentialsGrantType.java index 17543ad7af..9cdc26a09e 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/ClientCredentialsGrantType.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ClientCredentialsGrantType.java @@ -180,7 +180,7 @@ public class ClientCredentialsGrantType extends OAuth2GrantTypeBase { } event.success(); - return cors.builder(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).build(); + return cors.add(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)); } @Override diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantTypeBase.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantTypeBase.java index 5fe0fe0378..19b2e9a9ef 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantTypeBase.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantTypeBase.java @@ -159,7 +159,7 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType { event.success(); - return cors.builder(Response.ok(res).type(MediaType.APPLICATION_JSON_TYPE)).build(); + return cors.add(Response.ok(res).type(MediaType.APPLICATION_JSON_TYPE)); } protected void checkAndBindMtlsHoKToken(TokenManager.AccessTokenResponseBuilder responseBuilder, boolean useRefreshToken) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/PreAuthorizedCodeGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/PreAuthorizedCodeGrantType.java index 2a0f510f06..60534a08f1 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/PreAuthorizedCodeGrantType.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/PreAuthorizedCodeGrantType.java @@ -96,7 +96,7 @@ public class PreAuthorizedCodeGrantType extends OAuth2GrantTypeBase { event.success(); - return cors.allowAllOrigins().builder(Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE)).build(); + return cors.allowAllOrigins().add(Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE)); } @Override diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/RefreshTokenGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/RefreshTokenGrantType.java index efafc320ce..608da84814 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/RefreshTokenGrantType.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/RefreshTokenGrantType.java @@ -108,7 +108,7 @@ public class RefreshTokenGrantType extends OAuth2GrantTypeBase { event.success(); - return cors.builder(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).build(); + return cors.add(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)); } @Override diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ResourceOwnerPasswordCredentialsGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ResourceOwnerPasswordCredentialsGrantType.java index 81b36bf09a..e3fce2e6c1 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/ResourceOwnerPasswordCredentialsGrantType.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ResourceOwnerPasswordCredentialsGrantType.java @@ -31,7 +31,6 @@ import org.keycloak.events.EventType; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.ClientSessionContext; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.AuthenticationFlowResolver; @@ -113,7 +112,7 @@ public class ResourceOwnerPasswordCredentialsGrantType extends OAuth2GrantTypeBa if (challenge != null) { // Remove authentication session as "Resource Owner Password Credentials Grant" is single-request scoped authentication new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, false); - cors.build(response); + cors.add(); return challenge; } processor.evaluateRequiredActionTriggers(); @@ -161,7 +160,7 @@ public class ResourceOwnerPasswordCredentialsGrantType extends OAuth2GrantTypeBa event.success(); AuthenticationManager.logSuccess(session, authSession); - return cors.builder(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).build(); + return cors.add(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)); } @Override diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/endpoints/DeviceEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/endpoints/DeviceEndpoint.java index 0f21197ce0..42b7a046b7 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/endpoints/DeviceEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/endpoints/DeviceEndpoint.java @@ -101,7 +101,7 @@ public class DeviceEndpoint extends AuthorizationEndpointBase implements RealmRe @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.APPLICATION_JSON) public Response handleDeviceRequest() { - cors = Cors.add(request).auth().allowedMethods("POST").auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS); + cors = Cors.builder().auth().allowedMethods("POST").auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS); logger.trace("Processing @POST request"); event.event(EventType.OAUTH2_DEVICE_AUTH); @@ -186,7 +186,7 @@ public class DeviceEndpoint extends AuthorizationEndpointBase implements RealmRe response.setVerificationUri(deviceUrl); response.setVerificationUriComplete(deviceUrl + "?user_code=" + response.getUserCode()); - return cors.builder(Response.ok(JsonSerialization.writeValueAsBytes(response)).type(MediaType.APPLICATION_JSON_TYPE)).build(); + return cors.add(Response.ok(JsonSerialization.writeValueAsBytes(response)).type(MediaType.APPLICATION_JSON_TYPE)); } catch (Exception e) { throw new RuntimeException("Error creating OAuth 2.0 Device Authorization Response.", e); } @@ -197,7 +197,7 @@ public class DeviceEndpoint extends AuthorizationEndpointBase implements RealmRe if (logger.isDebugEnabled()) { logger.debugv("CORS preflight from: {0}", headers.getRequestHeaders().getFirst("Origin")); } - return Cors.add(request, Response.ok()).auth().preflight().allowedMethods("POST", "OPTIONS").build(); + return Cors.builder().auth().preflight().allowedMethods("POST", "OPTIONS").add(Response.ok()); } /** diff --git a/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/ParEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/ParEndpoint.java index e94639fb81..188aad9e3f 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/ParEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/ParEndpoint.java @@ -80,7 +80,7 @@ public class ParEndpoint extends AbstractParEndpoint { ProfileHelper.requireFeature(Profile.Feature.PAR); - cors = Cors.add(httpRequest).auth().allowedMethods("POST").auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS); + cors = Cors.builder().auth().allowedMethods("POST").auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS); event.event(EventType.PUSHED_AUTHORIZATION_REQUEST); @@ -163,10 +163,9 @@ public class ParEndpoint extends AbstractParEndpoint { ParResponse parResponse = new ParResponse(requestUri, expiresIn); session.getProvider(SecurityHeadersProvider.class).options().allowEmptyContentType(); - return cors.builder(Response.status(Response.Status.CREATED) - .entity(parResponse) - .type(MediaType.APPLICATION_JSON_TYPE)) - .build(); + return cors.add(Response.status(Response.Status.CREATED) + .entity(parResponse) + .type(MediaType.APPLICATION_JSON_TYPE)); } } \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java index f7063d14e9..3a75254af1 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java @@ -18,7 +18,6 @@ package org.keycloak.protocol.oidc.utils; import org.jboss.logging.Logger; -import org.keycloak.http.HttpResponse; import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.ClientAuthenticator; import org.keycloak.authentication.ClientAuthenticatorFactory; @@ -51,8 +50,7 @@ public class AuthorizeClientUtil { if (response != null) { if (cors != null) { cors.allowAllOrigins(); - HttpResponse httpResponse = session.getContext().getHttpResponse(); - cors.build(httpResponse); + cors.add(); } throw new WebApplicationException(response); } diff --git a/services/src/main/java/org/keycloak/services/CorsErrorResponseException.java b/services/src/main/java/org/keycloak/services/CorsErrorResponseException.java index e2d903c2da..f2ed784851 100644 --- a/services/src/main/java/org/keycloak/services/CorsErrorResponseException.java +++ b/services/src/main/java/org/keycloak/services/CorsErrorResponseException.java @@ -49,7 +49,7 @@ public class CorsErrorResponseException extends WebApplicationException { public Response getResponse() { OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(error, errorDescription); Response.ResponseBuilder builder = Response.status(status).entity(errorRep).type(MediaType.APPLICATION_JSON_TYPE); - return cors.builder(builder).build(); + return cors.add(builder); } } 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 45b3dd869a..d33488ab71 100755 --- a/services/src/main/java/org/keycloak/services/cors/DefaultCors.java +++ b/services/src/main/java/org/keycloak/services/cors/DefaultCors.java @@ -21,9 +21,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; -import java.util.function.BiConsumer; -import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.ResponseBuilder; import org.jboss.logging.Logger; @@ -42,7 +40,8 @@ public class DefaultCors implements Cors { private static final Logger logger = Logger.getLogger(DefaultCors.class); - private HttpRequest request; + private final HttpRequest request; + private final HttpResponse response; private ResponseBuilder builder; private Set allowedOrigins; private Set allowedMethods; @@ -51,14 +50,9 @@ public class DefaultCors implements Cors { private boolean preflight; private boolean auth; - DefaultCors(HttpRequest request) { - this.request = request; - } - - @Override - public Cors request(HttpRequest request) { - this.request = request; - return this; + DefaultCors(KeycloakSession session) { + this.request = session.getContext().getHttpRequest(); + this.response = session.getContext().getHttpResponse(); } @Override @@ -117,89 +111,65 @@ public class DefaultCors implements Cors { @Override public Cors exposedHeaders(String... exposedHeaders) { - this.exposedHeaders = new HashSet<>(Arrays.asList(exposedHeaders)); - return this; - } - - @Override - public Cors addExposedHeaders(String... exposedHeaders) { if (this.exposedHeaders == null) { - this.exposedHeaders(exposedHeaders); - } else { - this.exposedHeaders.addAll(Arrays.asList(exposedHeaders)); + this.exposedHeaders = new HashSet<>(); } + + this.exposedHeaders.addAll(Arrays.asList(exposedHeaders)); + return this; } @Override - public Response build() { - if (builder == null) { - throw new IllegalStateException("builder is not set"); - } - - if (build(builder::header)) { - logger.debug("Added CORS headers to response"); - } - return builder.build(); - } - - @Override - public boolean build(HttpResponse response) { - if (build(response::addHeader)) { - logger.debug("Added CORS headers to response"); - return true; - } - return false; - } - - @Override - public boolean build(BiConsumer addHeader) { + public void add() { if (request == null) { throw new IllegalStateException("request is not set"); } + if (response == null) { + throw new IllegalStateException("response is not set"); + } + String origin = request.getHttpHeaders().getRequestHeaders().getFirst(ORIGIN_HEADER); if (origin == null) { logger.trace("No Origin header, ignoring"); - return false; + return; } if (!preflight && (allowedOrigins == null || (!allowedOrigins.contains(origin) && !allowedOrigins.contains(ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD)))) { if (logger.isDebugEnabled()) { logger.debugv("Invalid CORS request: origin {0} not in allowed origins {1}", origin, allowedOrigins); } - return false; + return; } - addHeader.accept(ACCESS_CONTROL_ALLOW_ORIGIN, origin); + response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin); if (preflight) { if (allowedMethods != null) { - addHeader.accept(ACCESS_CONTROL_ALLOW_METHODS, CollectionUtil.join(allowedMethods)); + response.setHeader(ACCESS_CONTROL_ALLOW_METHODS, CollectionUtil.join(allowedMethods)); } else { - addHeader.accept(ACCESS_CONTROL_ALLOW_METHODS, DEFAULT_ALLOW_METHODS); + response.setHeader(ACCESS_CONTROL_ALLOW_METHODS, DEFAULT_ALLOW_METHODS); } } if (!preflight && exposedHeaders != null) { - addHeader.accept(ACCESS_CONTROL_EXPOSE_HEADERS, CollectionUtil.join(exposedHeaders)); + response.setHeader(ACCESS_CONTROL_EXPOSE_HEADERS, CollectionUtil.join(exposedHeaders)); } - addHeader.accept(ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.toString(auth)); + response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.toString(auth)); if (preflight) { if (auth) { - addHeader.accept(ACCESS_CONTROL_ALLOW_HEADERS, String.format("%s, %s", DEFAULT_ALLOW_HEADERS, AUTHORIZATION_HEADER)); + response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS, String.format("%s, %s", DEFAULT_ALLOW_HEADERS, AUTHORIZATION_HEADER)); } else { - addHeader.accept(ACCESS_CONTROL_ALLOW_HEADERS, DEFAULT_ALLOW_HEADERS); + response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS, DEFAULT_ALLOW_HEADERS); } } if (preflight) { - addHeader.accept(ACCESS_CONTROL_MAX_AGE, String.valueOf(DEFAULT_MAX_AGE)); + response.setHeader(ACCESS_CONTROL_MAX_AGE, String.valueOf(DEFAULT_MAX_AGE)); } - - return true; } @Override 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 de301f5571..17877512aa 100644 --- a/services/src/main/java/org/keycloak/services/cors/DefaultCorsFactory.java +++ b/services/src/main/java/org/keycloak/services/cors/DefaultCorsFactory.java @@ -30,7 +30,7 @@ public class DefaultCorsFactory implements CorsFactory { @Override public Cors create(KeycloakSession session) { - return new DefaultCors(session.getContext().getHttpRequest()); + return new DefaultCors(session); } @Override diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index 537471f446..7a1bc7e29f 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -448,7 +448,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal @Path("{provider_alias}/token") @OPTIONS public Response retrieveTokenPreflight() { - return Cors.add(this.request, Response.ok()).auth().preflight().build(); + return Cors.builder().auth().preflight().add(Response.ok()); } @GET @@ -1346,7 +1346,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal } private Response corsResponse(Response response, ClientModel clientModel) { - return Cors.add(this.request, Response.fromResponse(response)).auth().allowedOrigins(session, clientModel).build(); + return Cors.builder().auth().allowedOrigins(session, clientModel).add(Response.fromResponse(response)); } private void fireErrorEvent(String message, Throwable throwable) { diff --git a/services/src/main/java/org/keycloak/services/resources/JsResource.java b/services/src/main/java/org/keycloak/services/resources/JsResource.java index 0ead07a1a2..fd46429fd3 100755 --- a/services/src/main/java/org/keycloak/services/resources/JsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/JsResource.java @@ -127,7 +127,7 @@ public class JsResource { } String contentType = "text/javascript"; - Cors cors = Cors.add(session.getContext().getHttpRequest()).allowAllOrigins(); + Cors cors = Cors.builder().allowAllOrigins(); ResourceEncodingProvider encodingProvider = ResourceEncodingHelper.getResourceEncodingProvider(session, contentType); @@ -143,9 +143,9 @@ public class JsResource { if (encodingProvider != null) { rb.encoding(encodingProvider.getEncoding()); } - return cors.builder(rb).build(); + return cors.add(rb); } else { - return cors.builder(Response.status(Response.Status.NOT_FOUND)).build(); + return cors.add(Response.status(Response.Status.NOT_FOUND)); } } } diff --git a/services/src/main/java/org/keycloak/services/resources/PublicRealmResource.java b/services/src/main/java/org/keycloak/services/resources/PublicRealmResource.java index 39de90191a..a1b182e5b7 100755 --- a/services/src/main/java/org/keycloak/services/resources/PublicRealmResource.java +++ b/services/src/main/java/org/keycloak/services/resources/PublicRealmResource.java @@ -68,7 +68,7 @@ public class PublicRealmResource { @Path("/") @OPTIONS public Response accountPreflight() { - return Cors.add(request, Response.ok()).auth().preflight().build(); + return Cors.builder().auth().preflight().add(Response.ok()); } /** @@ -80,7 +80,7 @@ public class PublicRealmResource { @NoCache @Produces(MediaType.APPLICATION_JSON) public PublishedRealmRepresentation getRealm() { - Cors.add(request).allowedOrigins(Cors.ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD).auth().build(response); + Cors.builder().allowedOrigins(Cors.ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD).auth().add(); return realmRep(session, realm, session.getContext().getUri()); } diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java index 99fc6d6f3c..7542f27133 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -217,7 +217,7 @@ public class RealmsResource { @Produces(MediaType.APPLICATION_JSON) public Response getVersionPreflight(final @PathParam("realm") String name, final @PathParam("provider") String providerName) { - return Cors.add(session.getContext().getHttpRequest(), Response.ok()).allowedMethods("GET").preflight().auth().build(); + return Cors.builder().allowedMethods("GET").preflight().auth().add(Response.ok()); } @GET @@ -240,7 +240,7 @@ public class RealmsResource { if (wellKnown != null) { ResponseBuilder responseBuilder = Response.ok(wellKnown.getConfig()).cacheControl(CacheControlUtil.noCache()); - return Cors.add(session.getContext().getHttpRequest(), responseBuilder).allowedOrigins("*").auth().build(); + return Cors.builder().allowedOrigins("*").auth().add(responseBuilder); } throw new NotFoundException(); @@ -279,7 +279,7 @@ public class RealmsResource { if (!"https".equals(session.getContext().getUri().getBaseUri().getScheme()) && realm.getSslRequired().isRequired(session.getContext().getConnection())) { HttpRequest request = session.getContext().getHttpRequest(); - Cors cors = Cors.add(request).auth().allowedMethods(request.getHttpMethod()).auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS); + Cors cors = Cors.builder().auth().allowedMethods(request.getHttpMethod()).auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS); throw new CorsErrorResponseException(cors.allowAllOrigins(), OAuthErrorException.INVALID_REQUEST, "HTTPS required", Response.Status.FORBIDDEN); } diff --git a/services/src/main/java/org/keycloak/services/resources/ThemeResource.java b/services/src/main/java/org/keycloak/services/resources/ThemeResource.java index e676aeb9f3..a8ce7363c4 100644 --- a/services/src/main/java/org/keycloak/services/resources/ThemeResource.java +++ b/services/src/main/java/org/keycloak/services/resources/ThemeResource.java @@ -109,7 +109,7 @@ public class ThemeResource { @Path("/{realm}/{themeType}/{locale}") @OPTIONS public Response localizationTextPreflight() { - return Cors.add(session.getContext().getHttpRequest(), Response.ok()).auth().preflight().build(); + return Cors.builder().auth().preflight().add(Response.ok()); } @GET @@ -151,8 +151,7 @@ public class ThemeResource { new KeySource((String) e.getKey(), (String) e.getValue())).collect(toList()); } - Response.ResponseBuilder responseBuilder = Response.ok(result); - return Cors.add(session.getContext().getHttpRequest(), responseBuilder).allowedOrigins("*").auth().build(); + return Cors.builder().allowedOrigins("*").auth().add(Response.ok(result)); } } diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountLoader.java b/services/src/main/java/org/keycloak/services/resources/account/AccountLoader.java index 250e19e011..50b9144eff 100644 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountLoader.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountLoader.java @@ -86,7 +86,7 @@ public class AccountLoader { AccountResourceProvider accountResourceProvider = getAccountResourceProvider(theme); if (request.getHttpMethod().equals(HttpMethod.OPTIONS)) { - return new CorsPreflightService(request); + return new CorsPreflightService(); } else if ((accepts.contains(MediaType.APPLICATION_JSON_TYPE) || MediaType.APPLICATION_JSON_TYPE.equals(content)) && !uriInfo.getPath().endsWith("keycloak.json")) { return getAccountRestService(client, null); } else if (accountResourceProvider != null) { @@ -100,7 +100,7 @@ public class AccountLoader { @Produces(MediaType.APPLICATION_JSON) public Object getVersionedAccountRestService(final @PathParam("version") String version) { if (request.getHttpMethod().equals(HttpMethod.OPTIONS)) { - return new CorsPreflightService(request); + return new CorsPreflightService(); } return getAccountRestService(getAccountManagementClient(session.getContext().getRealm()), version); } @@ -137,7 +137,7 @@ public class AccountLoader { Auth auth = new Auth(session.getContext().getRealm(), accessToken, authResult.getUser(), client, authResult.getSession(), false); - Cors.add(request).allowedOrigins(auth.getToken()).allowedMethods("GET", "PUT", "POST", "DELETE").auth().build(response); + Cors.builder().allowedOrigins(auth.getToken()).allowedMethods("GET", "PUT", "POST", "DELETE").auth().add(); if (authResult.getUser().getServiceAccountClientLink() != null) { throw new NotAuthorizedException("Service accounts are not allowed to access this service"); diff --git a/services/src/main/java/org/keycloak/services/resources/account/CorsPreflightService.java b/services/src/main/java/org/keycloak/services/resources/account/CorsPreflightService.java index c205caab8f..a1da7b3d45 100644 --- a/services/src/main/java/org/keycloak/services/resources/account/CorsPreflightService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/CorsPreflightService.java @@ -1,6 +1,5 @@ package org.keycloak.services.resources.account; -import org.keycloak.http.HttpRequest; import org.keycloak.services.cors.Cors; import jakarta.ws.rs.OPTIONS; @@ -12,12 +11,6 @@ import jakarta.ws.rs.core.Response; */ public class CorsPreflightService { - private final HttpRequest request; - - public CorsPreflightService(HttpRequest request) { - this.request = request; - } - /** * CORS preflight * @@ -26,8 +19,8 @@ public class CorsPreflightService { @Path("{any:.*}") @OPTIONS public Response preflight() { - Cors cors = Cors.add(request, Response.ok()).auth().allowedMethods("GET", "POST", "DELETE", "PUT", "HEAD", "OPTIONS").preflight(); - return cors.build(); + return Cors.builder().auth().allowedMethods("GET", "POST", "DELETE", "PUT", "HEAD", "OPTIONS").preflight() + .add(Response.ok()); } } diff --git a/services/src/main/java/org/keycloak/services/resources/account/LinkedAccountsResource.java b/services/src/main/java/org/keycloak/services/resources/account/LinkedAccountsResource.java index dcf796c5e6..215c073baa 100644 --- a/services/src/main/java/org/keycloak/services/resources/account/LinkedAccountsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/account/LinkedAccountsResource.java @@ -99,7 +99,7 @@ public class LinkedAccountsResource { public Response linkedAccounts() { auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE); SortedSet linkedAccounts = getLinkedAccounts(this.session, this.realm, this.user); - return Cors.add(request, Response.ok(linkedAccounts)).auth().allowedOrigins(auth.getToken()).build(); + return Cors.builder().auth().allowedOrigins(auth.getToken()).add(Response.ok(linkedAccounts)); } private Set findSocialIds() { @@ -183,7 +183,7 @@ public class LinkedAccountsResource { rep.setHash(hash); rep.setNonce(nonce); - return Cors.add(request, Response.ok(rep)).auth().allowedOrigins(auth.getToken()).build(); + return Cors.builder().auth().allowedOrigins(auth.getToken()).add(Response.ok(rep)); } catch (Exception spe) { spe.printStackTrace(); throw ErrorResponse.error(Messages.FAILED_TO_PROCESS_RESPONSE, Response.Status.INTERNAL_SERVER_ERROR); @@ -221,7 +221,7 @@ public class LinkedAccountsResource { .detail(Details.IDENTITY_PROVIDER_USERNAME, link.getUserName()) .success(); - return Cors.add(request, Response.noContent()).auth().allowedOrigins(auth.getToken()).build(); + return Cors.builder().auth().allowedOrigins(auth.getToken()).add(Response.noContent()); } private String checkCommonPreconditions(String providerAlias) { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java index aa62abd2f8..499c5a2634 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java @@ -185,7 +185,7 @@ public class AdminConsole { @Path("whoami") @OPTIONS public Response whoAmIPreFlight() { - return new AdminCorsPreflightService(request).preflight(); + return new AdminCorsPreflightService().preflight(); } /** @@ -239,10 +239,11 @@ public class AdminConsole { Locale locale = session.getContext().resolveLocale(user); - Cors.add(request).allowedOrigins(authResult.getToken()).allowedMethods("GET").auth() - .build(response); - - return Response.ok(new WhoAmI(user.getId(), realm.getName(), displayName, createRealm, realmAccess, locale)).build(); + return Cors.builder() + .allowedOrigins(authResult.getToken()) + .allowedMethods("GET") + .auth() + .add(Response.ok(new WhoAmI(user.getId(), realm.getName(), displayName, createRealm, realmAccess, locale))); } private void addRealmAccess(RealmModel realm, UserModel user, Map> realmAdminAccess) { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminCorsPreflightService.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminCorsPreflightService.java index 57ac467637..de223994bc 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminCorsPreflightService.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminCorsPreflightService.java @@ -1,6 +1,5 @@ package org.keycloak.services.resources.admin; -import org.keycloak.http.HttpRequest; import org.keycloak.services.cors.Cors; import jakarta.ws.rs.OPTIONS; @@ -12,12 +11,6 @@ import jakarta.ws.rs.core.Response; */ public class AdminCorsPreflightService { - private HttpRequest request; - - public AdminCorsPreflightService(HttpRequest request) { - this.request = request; - } - /** * CORS preflight * @@ -26,7 +19,7 @@ public class AdminCorsPreflightService { @Path("{any:.*}") @OPTIONS public Response preflight() { - return Cors.add(request, Response.ok()).preflight().allowedMethods("GET", "PUT", "POST", "DELETE").auth().build(); + return Cors.builder().preflight().allowedMethods("GET", "PUT", "POST", "DELETE").auth().add(Response.ok()); } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java index 1aff7d9600..d5262d4917 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java @@ -224,10 +224,7 @@ public class AdminRoot { logger.debug("authenticated admin access for: " + auth.getUser().getUsername()); } - HttpResponse response = getHttpResponse(); - - Cors.add(request).allowedOrigins(auth.getToken()).allowedMethods("GET", "PUT", "POST", "DELETE").exposedHeaders("Location").auth().build( - response); + Cors.builder().allowedOrigins(auth.getToken()).allowedMethods("GET", "PUT", "POST", "DELETE").exposedHeaders("Location").auth().add(); return new RealmsAdminResource(session, auth, tokenManager); } @@ -236,13 +233,11 @@ public class AdminRoot { @OPTIONS @Operation(hidden = true) public Object preFlight() { - HttpRequest request = getHttpRequest(); - if (!isAdminApiEnabled()) { throw new NotFoundException(); } - return new AdminCorsPreflightService(request); + return new AdminCorsPreflightService(); } /** @@ -261,7 +256,7 @@ public class AdminRoot { HttpRequest request = getHttpRequest(); if (request.getHttpMethod().equals(HttpMethod.OPTIONS)) { - return new AdminCorsPreflightService(request); + return new AdminCorsPreflightService(); } AdminAuth auth = authenticateRealmAdminRequest(session.getContext().getRequestHeaders()); @@ -273,8 +268,7 @@ public class AdminRoot { logger.debug("authenticated admin access for: " + auth.getUser().getUsername()); } - Cors.add(request).allowedOrigins(auth.getToken()).allowedMethods("GET", "PUT", "POST", "DELETE").auth().build( - getHttpResponse()); + Cors.builder().allowedOrigins(auth.getToken()).allowedMethods("GET", "PUT", "POST", "DELETE").auth().add(); return new ServerInfoAdminResource(session); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResourcePreflight.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResourcePreflight.java index 79e8c26b9d..646a05c6fc 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResourcePreflight.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResourcePreflight.java @@ -41,7 +41,7 @@ public class RealmsAdminResourcePreflight extends RealmsAdminResource { @Path("{any:.*}") @OPTIONS public Response preFlight() { - return Cors.add(request, Response.ok()).preflight().allowedMethods("GET", "PUT", "POST", "DELETE").auth().build(); + return Cors.builder().preflight().allowedMethods("GET", "PUT", "POST", "DELETE").auth().add(Response.ok()); } } diff --git a/services/src/main/java/org/keycloak/utils/OAuth2Error.java b/services/src/main/java/org/keycloak/utils/OAuth2Error.java index 71fd1cbf8b..f59cb7e16d 100644 --- a/services/src/main/java/org/keycloak/utils/OAuth2Error.java +++ b/services/src/main/java/org/keycloak/utils/OAuth2Error.java @@ -136,10 +136,10 @@ public class OAuth2Error { bearer.setErrorDescription(errorDescription); WWWAuthenticate wwwAuthenticate = new WWWAuthenticate(bearer); wwwAuthenticate.build(builder::header); - cors.ifPresent(_cors -> _cors.addExposedHeaders(WWW_AUTHENTICATE)); + cors.ifPresent(_cors -> _cors.exposedHeaders(WWW_AUTHENTICATE)); builder.entity("").type(MediaType.TEXT_PLAIN_UTF_8_TYPE); } - cors.ifPresent(_cors -> { _cors.build(builder::header); }); + cors.ifPresent(Cors::add); return constructor.newInstance(builder.build()); } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java index 91da63421a..86090b9735 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java @@ -17,6 +17,9 @@ package org.keycloak.testsuite.account; import com.fasterxml.jackson.core.type.TypeReference; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import org.apache.http.Header; import org.apache.http.impl.client.CloseableHttpClient; import org.hamcrest.Matchers; import org.junit.Assert; @@ -62,6 +65,7 @@ import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RequiredActionProviderRepresentation; import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation; +import org.keycloak.services.cors.Cors; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.account.AccountCredentialResource; import org.keycloak.services.util.ResolveRelative; @@ -81,9 +85,11 @@ import jakarta.ws.rs.core.Response; import java.io.IOException; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import static org.hamcrest.CoreMatchers.is; @@ -592,6 +598,26 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { } } + @Test + public void testCors() throws IOException { + String accountUrl = getAccountUrl(null); + SimpleHttp a = SimpleHttpDefault.doGet(accountUrl + "/linked-accounts", httpClient).auth(tokenUtil.getToken()) + .header("Origin", "http://localtest.me:8180") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); + + try (SimpleHttp.Response response = a.asResponse()) { + Set expected = new HashSet<>(); + Header[] actual = response.getAllHeaders(); + + for (Header header : actual) { + assertTrue(expected.add(header.getName())); + } + + assertThat(expected, Matchers.hasItems(Cors.ACCESS_CONTROL_ALLOW_ORIGIN, Cors.ACCESS_CONTROL_ALLOW_CREDENTIALS)); + } + + } + protected UserRepresentation getUser() throws IOException { return getUser(true); }