Skip to content

Commit

Permalink
Simplifying the CORS SPI and the default implementation
Browse files Browse the repository at this point in the history
Closes keycloak#27646

Signed-off-by: Pedro Igor <[email protected]>
  • Loading branch information
pedroigor committed May 7, 2024
1 parent c18a68b commit 2cd9bec
Show file tree
Hide file tree
Showing 33 changed files with 170 additions and 193 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,59 +34,67 @@
*/
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);
}

public Cors request(HttpRequest request);
Cors builder(ResponseBuilder builder);

public Cors builder(ResponseBuilder builder);
Cors preflight();

public Cors preflight();
Cors auth();

public Cors auth();
Cors allowAllOrigins();

public Cors allowAllOrigins();
Cors allowedOrigins(KeycloakSession session, ClientModel client);

public Cors allowedOrigins(KeycloakSession session, ClientModel client);
Cors allowedOrigins(AccessToken token);

public Cors allowedOrigins(AccessToken token);
Cors allowedOrigins(String... allowedOrigins);

public Cors allowedOrigins(String... allowedOrigins);
Cors allowedMethods(String... allowedMethods);

public Cors allowedMethods(String... allowedMethods);
Cors exposedHeaders(String... exposedHeaders);

public Cors exposedHeaders(String... exposedHeaders);
/**
* Add the CORS headers to the current {@link org.keycloak.http.HttpResponse}.
*/
void add();

public Cors addExposedHeaders(String... exposedHeaders);
/**
* <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");
}

public Response build();

public boolean build(HttpResponse response);

public boolean build(BiConsumer<String, String> addHeader);
add();

return builder.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,11 @@ public Response authorize(KeycloakAuthorizationRequest request) {
}

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ protected Response exchangeToIdentityProvider(UserModel targetUser, UserSessionM
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));

}

Expand Down Expand Up @@ -451,7 +451,7 @@ protected Response exchangeClientToOIDCClient(UserModel targetUser, UserSessionM

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) {
Expand Down Expand Up @@ -501,7 +501,7 @@ protected Response exchangeClientToSAML2Client(UserModel targetUser, UserSession

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ public Object thirdPartyCookiesCheck() {
@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
Expand Down Expand Up @@ -232,7 +232,7 @@ public Response certs() {
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")
Expand Down Expand Up @@ -276,7 +276,7 @@ public Object resolveExtension(@PathParam("extension") String extension) {
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public LogoutEndpoint(KeycloakSession session, TokenManager tokenManager, EventB
@Path("/")
@OPTIONS
public Response issueUserInfoPreflight() {
return Cors.add(this.request, Response.ok()).auth().preflight().build();
return Cors.builder().auth().preflight().add(Response.ok());
}

/**
Expand Down Expand Up @@ -496,7 +496,7 @@ private Response doBrowserLogout(AuthenticationSessionModel logoutSession) {
* @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();
Expand Down Expand Up @@ -550,7 +550,7 @@ private Response logoutToken() {
}
}

return cors.builder(Response.noContent()).build();
return cors.add(Response.noContent());
}

/**
Expand Down Expand Up @@ -618,18 +618,16 @@ public Response backchannelLogout() {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public TokenEndpoint(KeycloakSession session, TokenManager tokenManager, EventBu
@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();

Expand Down Expand Up @@ -150,7 +150,7 @@ public Response preflight() {
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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public TokenRevocationEndpoint(KeycloakSession session, EventBuilder event) {
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();
Expand Down Expand Up @@ -130,12 +130,12 @@ public Response revoke() {
}

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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public UserInfoEndpoint(KeycloakSession session, org.keycloak.protocol.oidc.Toke
@Path("/")
@OPTIONS
public Response issueUserInfoPreflight() {
return Cors.add(this.request, Response.ok()).auth().preflight().build();
return Cors.builder().auth().preflight().add(Response.ok());
}

@Path("/")
Expand Down Expand Up @@ -322,7 +322,7 @@ private Response issueUserInfo() {

event.success();

return cors.builder(responseBuilder).build();
return cors.add(responseBuilder);
}

private String jweFromContent(String content, String jweContentType) {
Expand Down Expand Up @@ -363,7 +363,7 @@ private void checkAccessTokenDuplicated(MultivaluedMap<String, String> formParam
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ public Response process(Context context) {
}
event.success();

return cors.builder(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).build();
return cors.add(Response.ok(res, MediaType.APPLICATION_JSON_TYPE));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ protected Response createTokenResponse(UserModel user, UserSessionModel userSess

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public Response process(Context context) {

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public Response process(Context context) {

event.success();

return cors.builder(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).build();
return cors.add(Response.ok(res, MediaType.APPLICATION_JSON_TYPE));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
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;
Expand Down Expand Up @@ -113,7 +112,7 @@ public Response process(Context context) {
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();
Expand Down Expand Up @@ -161,7 +160,7 @@ public Response process(Context context) {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public DeviceEndpoint(KeycloakSession session, EventBuilder event) {
@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);
Expand Down Expand Up @@ -186,7 +186,7 @@ public Response handleDeviceRequest() {
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);
}
Expand All @@ -197,7 +197,7 @@ public Response preflight() {
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());
}

/**
Expand Down
Loading

0 comments on commit 2cd9bec

Please sign in to comment.