Simplifying the CORS SPI and the default implementation

Closes #27646

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2024-04-08 18:36:10 -03:00
parent cbce548e71
commit a65508ca13
33 changed files with 182 additions and 205 deletions

View file

@ -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();
/**
* <p>Add the CORS headers to the current server {@link org.keycloak.http.HttpResponse} and returns a {@link Response} based
* on the given {@code builder}.
*
* <p>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<String, String> addHeader);
}

View file

@ -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) {

View file

@ -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) {

View file

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

View file

@ -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<String, String> 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,

View file

@ -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<String, String> 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() {

View file

@ -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() {

View file

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

View file

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

View file

@ -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) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<String> allowedOrigins;
private Set<String> 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<String, String> 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

View file

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

View file

@ -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) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -99,7 +99,7 @@ public class LinkedAccountsResource {
public Response linkedAccounts() {
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
SortedSet<LinkedAccountRepresentation> 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<String> 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) {

View file

@ -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<String, Set<String>> realmAdminAccess) {

View file

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

View file

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

View file

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

View file

@ -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) {

View file

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