Closes #25446

Signed-off-by: Dmitry Telegin <demetrio@carretti.pro>
This commit is contained in:
Dmitry Telegin 2024-01-27 00:39:52 +00:00 committed by Pedro Igor
parent 509f618992
commit b0403e2268
44 changed files with 336 additions and 76 deletions

View file

@ -23,7 +23,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
import org.keycloak.services.resources.Cors; import org.keycloak.services.cors.Cors;
import org.keycloak.theme.FreeMarkerException; import org.keycloak.theme.FreeMarkerException;
import org.keycloak.theme.Theme; import org.keycloak.theme.Theme;
import org.keycloak.theme.freemarker.FreeMarkerProvider; import org.keycloak.theme.freemarker.FreeMarkerProvider;

View file

@ -23,6 +23,7 @@ import org.keycloak.events.EventBuilder;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.services.cors.Cors;
import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.MultivaluedMap;
@ -40,7 +41,7 @@ public class TokenExchangeContext {
private final MultivaluedMap<String, String> formParams; private final MultivaluedMap<String, String> formParams;
// TODO: resolve deps issue and use correct types // TODO: resolve deps issue and use correct types
private final Object cors; private final Cors cors;
private final Object tokenManager; private final Object tokenManager;
private final ClientModel client; private final ClientModel client;
@ -55,7 +56,7 @@ public class TokenExchangeContext {
public TokenExchangeContext(KeycloakSession session, public TokenExchangeContext(KeycloakSession session,
MultivaluedMap<String, String> formParams, MultivaluedMap<String, String> formParams,
Object cors, Cors cors,
RealmModel realm, RealmModel realm,
EventBuilder event, EventBuilder event,
ClientModel client, ClientModel client,
@ -83,7 +84,7 @@ public class TokenExchangeContext {
return formParams; return formParams;
} }
public Object getCors() { public Cors getCors() {
return cors; return cors;
} }

View file

@ -0,0 +1,92 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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.common.util.Resteasy;
import org.keycloak.http.HttpRequest;
import org.keycloak.http.HttpResponse;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.Provider;
import org.keycloak.representations.AccessToken;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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";
public static final String ORIGIN_HEADER = "Origin";
public static final 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";
public static final String ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD = "*";
public static final String INCLUDE_REDIRECTS = "+";
public static Cors add(HttpRequest request, ResponseBuilder response) {
KeycloakSession session = Resteasy.getContextData(KeycloakSession.class);
return session.getProvider(Cors.class).request(request).builder(response);
}
public static Cors add(HttpRequest request) {
KeycloakSession session = Resteasy.getContextData(KeycloakSession.class);
return session.getProvider(Cors.class).request(request);
}
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 Response build();
public boolean build(HttpResponse response);
public boolean build(BiConsumer<String, String> addHeader);
}

View file

@ -0,0 +1,27 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.services.cors;
import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:demetrio@carretti.pro">Dmitry Telegin</a>
*/
public interface CorsFactory extends ProviderFactory<Cors> {
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.services.cors;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:demetrio@carretti.pro">Dmitry Telegin</a>
*/
public class CorsSpi implements Spi {
private static final String SPI_ID = "cors";
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return SPI_ID;
}
@Override
public Class<? extends Provider> getProviderClass() {
return Cors.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return CorsFactory.class;
}
}

View file

@ -90,6 +90,7 @@ org.keycloak.headers.SecurityHeadersSpi
org.keycloak.services.clientpolicy.condition.ClientPolicyConditionSpi org.keycloak.services.clientpolicy.condition.ClientPolicyConditionSpi
org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorSpi org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorSpi
org.keycloak.services.clientpolicy.ClientPolicyManagerSpi org.keycloak.services.clientpolicy.ClientPolicyManagerSpi
org.keycloak.services.cors.CorsSpi
org.keycloak.userprofile.UserProfileSpi org.keycloak.userprofile.UserProfileSpi
org.keycloak.device.DeviceRepresentationSpi org.keycloak.device.DeviceRepresentationSpi
org.keycloak.health.LoadBalancerCheckSpi org.keycloak.health.LoadBalancerCheckSpi

View file

@ -91,10 +91,10 @@ import org.keycloak.representations.idm.authorization.PermissionTicketToken;
import org.keycloak.services.CorsErrorResponseException; import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.resources.Cors;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel; import org.keycloak.sessions.RootAuthenticationSessionModel;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;

View file

@ -60,11 +60,11 @@ import org.keycloak.representations.JsonWebToken;
import org.keycloak.saml.common.constants.GeneralConstants; import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.services.CorsErrorResponseException; import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.resources.Cors;
import org.keycloak.services.resources.IdentityBrokerService; import org.keycloak.services.resources.IdentityBrokerService;
import org.keycloak.services.resources.admin.AdminAuth; import org.keycloak.services.resources.admin.AdminAuth;
import org.keycloak.services.resources.admin.permissions.AdminPermissions; import org.keycloak.services.resources.admin.permissions.AdminPermissions;
@ -120,7 +120,7 @@ public class DefaultTokenExchangeProvider implements TokenExchangeProvider {
public Response exchange(TokenExchangeContext context) { public Response exchange(TokenExchangeContext context) {
this.formParams = context.getFormParams(); this.formParams = context.getFormParams();
this.session = context.getSession(); this.session = context.getSession();
this.cors = (Cors)context.getCors(); this.cors = context.getCors();
this.realm = context.getRealm(); this.realm = context.getRealm();
this.client = context.getClient(); this.client = context.getClient();
this.event = context.getEvent(); this.event = context.getEvent();

View file

@ -42,7 +42,7 @@ import org.keycloak.protocol.oidc.endpoints.TokenRevocationEndpoint;
import org.keycloak.protocol.oidc.endpoints.UserInfoEndpoint; import org.keycloak.protocol.oidc.endpoints.UserInfoEndpoint;
import org.keycloak.protocol.oidc.ext.OIDCExtProvider; import org.keycloak.protocol.oidc.ext.OIDCExtProvider;
import org.keycloak.services.CorsErrorResponseException; import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.resources.Cors; import org.keycloak.services.cors.Cors;
import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.util.CacheControlUtil; import org.keycloak.services.util.CacheControlUtil;

View file

@ -47,8 +47,8 @@ import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.services.CorsErrorResponseException; import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.ErrorPageException; import org.keycloak.services.ErrorPageException;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.Cors;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.TokenUtil; import org.keycloak.util.TokenUtil;
import org.keycloak.utils.StringUtil; import org.keycloak.utils.StringUtil;

View file

@ -64,12 +64,12 @@ import org.keycloak.services.ErrorPage;
import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.clientpolicy.ClientPolicyException; import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.context.LogoutRequestContext; import org.keycloak.services.clientpolicy.context.LogoutRequestContext;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.Cors;
import org.keycloak.services.resources.LogoutSessionCodeChecks; import org.keycloak.services.resources.LogoutSessionCodeChecks;
import org.keycloak.services.resources.SessionCodeChecks; import org.keycloak.services.resources.SessionCodeChecks;
import org.keycloak.services.util.LocaleUtil; import org.keycloak.services.util.LocaleUtil;

View file

@ -85,13 +85,13 @@ import org.keycloak.services.clientpolicy.context.TokenRefreshContext;
import org.keycloak.services.clientpolicy.context.TokenRefreshResponseContext; import org.keycloak.services.clientpolicy.context.TokenRefreshResponseContext;
import org.keycloak.services.clientpolicy.context.TokenRequestContext; import org.keycloak.services.clientpolicy.context.TokenRequestContext;
import org.keycloak.services.clientpolicy.context.TokenResponseContext; import org.keycloak.services.clientpolicy.context.TokenResponseContext;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.resources.Cors;
import org.keycloak.services.util.AuthorizationContextUtil; import org.keycloak.services.util.AuthorizationContextUtil;
import org.keycloak.services.util.DefaultClientSessionContext; import org.keycloak.services.util.DefaultClientSessionContext;
import org.keycloak.services.util.DPoPUtil; import org.keycloak.services.util.DPoPUtil;

View file

@ -51,10 +51,10 @@ import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.clientpolicy.ClientPolicyException; import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.context.TokenRevokeContext; import org.keycloak.services.clientpolicy.context.TokenRevokeContext;
import org.keycloak.services.clientpolicy.context.TokenRevokeResponseContext; import org.keycloak.services.clientpolicy.context.TokenRevokeResponseContext;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.managers.UserConsentManager; import org.keycloak.services.managers.UserConsentManager;
import org.keycloak.services.managers.UserSessionCrossDCManager; import org.keycloak.services.managers.UserSessionCrossDCManager;
import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.resources.Cors;
import org.keycloak.util.TokenUtil; import org.keycloak.util.TokenUtil;
/** /**

View file

@ -56,8 +56,8 @@ import org.keycloak.representations.dpop.DPoP;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
import org.keycloak.services.clientpolicy.ClientPolicyException; import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.context.UserInfoRequestContext; import org.keycloak.services.clientpolicy.context.UserInfoRequestContext;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.resources.Cors;
import org.keycloak.services.util.DPoPUtil; import org.keycloak.services.util.DPoPUtil;
import org.keycloak.services.util.DefaultClientSessionContext; import org.keycloak.services.util.DefaultClientSessionContext;
import org.keycloak.services.util.MtlsHoKTokenUtil; import org.keycloak.services.util.MtlsHoKTokenUtil;

View file

@ -54,9 +54,9 @@ import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
import org.keycloak.services.clientpolicy.ClientPolicyException; import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.UserConsentManager; import org.keycloak.services.managers.UserConsentManager;
import org.keycloak.services.resources.Cors;
import org.keycloak.services.util.DefaultClientSessionContext; import org.keycloak.services.util.DefaultClientSessionContext;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel; import org.keycloak.sessions.RootAuthenticationSessionModel;

View file

@ -29,7 +29,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.services.resources.Cors; import org.keycloak.services.cors.Cors;
import org.keycloak.util.TokenUtil; import org.keycloak.util.TokenUtil;
/** /**

View file

@ -49,9 +49,9 @@ import org.keycloak.protocol.oidc.utils.PkceUtils;
import org.keycloak.services.CorsErrorResponseException; import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.clientpolicy.ClientPolicyException; import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.UserSessionCrossDCManager; import org.keycloak.services.managers.UserSessionCrossDCManager;
import org.keycloak.services.resources.Cors;
import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.util.DefaultClientSessionContext; import org.keycloak.services.util.DefaultClientSessionContext;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;

View file

@ -50,9 +50,9 @@ import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
import org.keycloak.services.clientpolicy.ClientPolicyException; import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resource.RealmResourceProvider; import org.keycloak.services.resource.RealmResourceProvider;
import org.keycloak.services.resources.Cors;
import org.keycloak.services.util.CacheControlUtil; import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;

View file

@ -31,7 +31,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.services.CorsErrorResponseException; import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.resources.Cors; import org.keycloak.services.cors.Cors;
public abstract class AbstractParEndpoint { public abstract class AbstractParEndpoint {

View file

@ -32,7 +32,7 @@ import org.keycloak.protocol.oidc.par.ParResponse;
import org.keycloak.protocol.oidc.par.clientpolicy.context.PushedAuthorizationRequestContext; import org.keycloak.protocol.oidc.par.clientpolicy.context.PushedAuthorizationRequestContext;
import org.keycloak.protocol.oidc.par.endpoints.request.ParEndpointRequestParserProcessor; import org.keycloak.protocol.oidc.par.endpoints.request.ParEndpointRequestParserProcessor;
import org.keycloak.services.clientpolicy.ClientPolicyException; import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.resources.Cors; import org.keycloak.services.cors.Cors;
import org.keycloak.utils.ProfileHelper; import org.keycloak.utils.ProfileHelper;
import jakarta.ws.rs.Consumes; import jakarta.ws.rs.Consumes;

View file

@ -31,7 +31,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.services.CorsErrorResponseException; import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.resources.Cors; import org.keycloak.services.cors.Cors;
import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;

View file

@ -9,7 +9,7 @@ import org.keycloak.events.Errors;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.services.CorsErrorResponseException; import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.services.resources.Cors; import org.keycloak.services.cors.Cors;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest; import java.security.MessageDigest;

View file

@ -18,7 +18,7 @@
package org.keycloak.services; package org.keycloak.services;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation; import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.services.resources.Cors; import org.keycloak.services.cors.Cors;
import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;

View file

@ -0,0 +1,53 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.services.cors;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:demetrio@carretti.pro">Dmitry Telegin</a>
*/
public class DefaultCorsFactory implements CorsFactory {
private static final String PROVIDER_ID = "default";
@Override
public Cors create(KeycloakSession session) {
return new DefaultCorsImpl(session.getContext().getHttpRequest());
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 Red Hat, Inc. and/or its affiliates * Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags. * and other contributors as indicated by the @author tags.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -14,16 +14,18 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.services.resources;
package org.keycloak.services.cors;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.ResponseBuilder; import jakarta.ws.rs.core.Response.ResponseBuilder;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.http.HttpRequest; import org.keycloak.http.HttpRequest;
import org.keycloak.common.util.CollectionUtil; import org.keycloak.common.util.CollectionUtil;
@ -36,26 +38,9 @@ import org.keycloak.representations.AccessToken;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class Cors { public class DefaultCorsImpl implements Cors {
private static final Logger logger = Logger.getLogger(Cors.class); private static final Logger logger = Logger.getLogger(DefaultCorsImpl.class);
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";
public static final String ORIGIN_HEADER = "Origin";
public static final 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";
public static final String ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD = "*";
public static final String INCLUDE_REDIRECTS = "+";
private HttpRequest request; private HttpRequest request;
private ResponseBuilder builder; private ResponseBuilder builder;
@ -66,43 +51,41 @@ public class Cors {
private boolean preflight; private boolean preflight;
private boolean auth; private boolean auth;
public Cors(HttpRequest request, ResponseBuilder response) { DefaultCorsImpl(HttpRequest request) {
this.request = request;
this.builder = response;
}
public Cors(HttpRequest request) {
this.request = request; this.request = request;
} }
public static Cors add(HttpRequest request, ResponseBuilder response) { @Override
return new Cors(request, response); public Cors request(HttpRequest request) {
} this.request = request;
return this;
public static Cors add(HttpRequest request) {
return new Cors(request);
} }
@Override
public Cors builder(ResponseBuilder builder) { public Cors builder(ResponseBuilder builder) {
this.builder = builder; this.builder = builder;
return this; return this;
} }
@Override
public Cors preflight() { public Cors preflight() {
preflight = true; preflight = true;
return this; return this;
} }
@Override
public Cors auth() { public Cors auth() {
auth = true; auth = true;
return this; return this;
} }
@Override
public Cors allowAllOrigins() { public Cors allowAllOrigins() {
allowedOrigins = Collections.singleton(ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD); allowedOrigins = Collections.singleton(ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD);
return this; return this;
} }
@Override
public Cors allowedOrigins(KeycloakSession session, ClientModel client) { public Cors allowedOrigins(KeycloakSession session, ClientModel client) {
if (client != null) { if (client != null) {
allowedOrigins = WebOriginsUtils.resolveValidWebOrigins(session, client); allowedOrigins = WebOriginsUtils.resolveValidWebOrigins(session, client);
@ -110,6 +93,7 @@ public class Cors {
return this; return this;
} }
@Override
public Cors allowedOrigins(AccessToken token) { public Cors allowedOrigins(AccessToken token) {
if (token != null) { if (token != null) {
allowedOrigins = token.getAllowedOrigins(); allowedOrigins = token.getAllowedOrigins();
@ -117,6 +101,7 @@ public class Cors {
return this; return this;
} }
@Override
public Cors allowedOrigins(String... allowedOrigins) { public Cors allowedOrigins(String... allowedOrigins) {
if (allowedOrigins != null && allowedOrigins.length > 0) { if (allowedOrigins != null && allowedOrigins.length > 0) {
this.allowedOrigins = new HashSet<>(Arrays.asList(allowedOrigins)); this.allowedOrigins = new HashSet<>(Arrays.asList(allowedOrigins));
@ -124,39 +109,56 @@ public class Cors {
return this; return this;
} }
@Override
public Cors allowedMethods(String... allowedMethods) { public Cors allowedMethods(String... allowedMethods) {
this.allowedMethods = new HashSet<>(Arrays.asList(allowedMethods)); this.allowedMethods = new HashSet<>(Arrays.asList(allowedMethods));
return this; return this;
} }
@Override
public Cors exposedHeaders(String... exposedHeaders) { public Cors exposedHeaders(String... exposedHeaders) {
this.exposedHeaders = new HashSet<>(Arrays.asList(exposedHeaders)); this.exposedHeaders = new HashSet<>(Arrays.asList(exposedHeaders));
return this; return this;
} }
@Override
public Response build() { public Response build() {
build(builder::header); if (builder == null) {
logger.debug("Added CORS headers to response"); throw new IllegalStateException("builder is not set");
}
if (build(builder::header)) {
logger.debug("Added CORS headers to response");
}
return builder.build(); return builder.build();
} }
public void build(HttpResponse response) { @Override
build(response::addHeader); public boolean build(HttpResponse response) {
logger.debug("Added CORS headers to response"); if (build(response::addHeader)) {
logger.debug("Added CORS headers to response");
return true;
}
return false;
} }
public void build(BiConsumer<String, String> addHeader) { @Override
public boolean build(BiConsumer<String, String> addHeader) {
if (request == null) {
throw new IllegalStateException("request is not set");
}
String origin = request.getHttpHeaders().getRequestHeaders().getFirst(ORIGIN_HEADER); String origin = request.getHttpHeaders().getRequestHeaders().getFirst(ORIGIN_HEADER);
if (origin == null) { if (origin == null) {
logger.trace("No origin header ignoring"); logger.trace("No Origin header, ignoring");
return; return false;
} }
if (!preflight && (allowedOrigins == null || (!allowedOrigins.contains(origin) && !allowedOrigins.contains(ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD)))) { if (!preflight && (allowedOrigins == null || (!allowedOrigins.contains(origin) && !allowedOrigins.contains(ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD)))) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debugv("Invalid CORS request: origin {0} not in allowed origins {1}", origin, allowedOrigins); logger.debugv("Invalid CORS request: origin {0} not in allowed origins {1}", origin, allowedOrigins);
} }
return; return false;
} }
addHeader.accept(ACCESS_CONTROL_ALLOW_ORIGIN, origin); addHeader.accept(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
@ -186,6 +188,12 @@ public class Cors {
if (preflight) { if (preflight) {
addHeader.accept(ACCESS_CONTROL_MAX_AGE, String.valueOf(DEFAULT_MAX_AGE)); addHeader.accept(ACCESS_CONTROL_MAX_AGE, String.valueOf(DEFAULT_MAX_AGE));
} }
return true;
}
@Override
public void close() {
} }
} }

View file

@ -82,6 +82,7 @@ import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.resources.account.AccountConsole; import org.keycloak.services.resources.account.AccountConsole;
import org.keycloak.services.util.AuthenticationFlowURLHelper; import org.keycloak.services.util.AuthenticationFlowURLHelper;
import org.keycloak.services.util.BrowserHistoryHelper; import org.keycloak.services.util.BrowserHistoryHelper;

View file

@ -21,6 +21,7 @@ import org.keycloak.common.Version;
import org.keycloak.encoding.ResourceEncodingHelper; import org.keycloak.encoding.ResourceEncodingHelper;
import org.keycloak.encoding.ResourceEncodingProvider; import org.keycloak.encoding.ResourceEncodingProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.util.CacheControlUtil; import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.utils.MediaType; import org.keycloak.utils.MediaType;

View file

@ -25,6 +25,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.idm.PublishedRealmRepresentation; import org.keycloak.representations.idm.PublishedRealmRepresentation;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;

View file

@ -31,6 +31,7 @@ import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocolFactory; import org.keycloak.protocol.LoginProtocolFactory;
import org.keycloak.services.CorsErrorResponseException; import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.clientregistration.ClientRegistrationService; import org.keycloak.services.clientregistration.ClientRegistrationService;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resource.RealmResourceProvider; import org.keycloak.services.resource.RealmResourceProvider;
import org.keycloak.services.resources.account.AccountLoader; import org.keycloak.services.resources.account.AccountLoader;

View file

@ -30,6 +30,7 @@ import org.keycloak.encoding.ResourceEncodingProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.util.CacheControlUtil; import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.services.util.LocaleUtil; import org.keycloak.services.util.LocaleUtil;
import org.keycloak.theme.Theme; import org.keycloak.theme.Theme;

View file

@ -26,11 +26,11 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.resource.AccountResourceProvider; import org.keycloak.services.resource.AccountResourceProvider;
import org.keycloak.services.resources.Cors;
import org.keycloak.theme.Theme; import org.keycloak.theme.Theme;
import jakarta.ws.rs.HttpMethod; import jakarta.ws.rs.HttpMethod;

View file

@ -1,7 +1,7 @@
package org.keycloak.services.resources.account; package org.keycloak.services.resources.account;
import org.keycloak.http.HttpRequest; import org.keycloak.http.HttpRequest;
import org.keycloak.services.resources.Cors; import org.keycloak.services.cors.Cors;
import jakarta.ws.rs.OPTIONS; import jakarta.ws.rs.OPTIONS;
import jakarta.ws.rs.Path; import jakarta.ws.rs.Path;

View file

@ -56,9 +56,9 @@ import org.keycloak.representations.account.AccountLinkUriRepresentation;
import org.keycloak.representations.account.LinkedAccountRepresentation; import org.keycloak.representations.account.LinkedAccountRepresentation;
import org.keycloak.services.ErrorResponse; import org.keycloak.services.ErrorResponse;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.Auth;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.Cors;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
import static org.keycloak.models.Constants.ACCOUNT_CONSOLE_CLIENT_ID; import static org.keycloak.models.Constants.ACCOUNT_CONSOLE_CLIENT_ID;

View file

@ -36,11 +36,11 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.Cors;
import org.keycloak.theme.FreeMarkerException; import org.keycloak.theme.FreeMarkerException;
import org.keycloak.theme.Theme; import org.keycloak.theme.Theme;
import org.keycloak.theme.freemarker.FreeMarkerProvider; import org.keycloak.theme.freemarker.FreeMarkerProvider;

View file

@ -1,7 +1,7 @@
package org.keycloak.services.resources.admin; package org.keycloak.services.resources.admin;
import org.keycloak.http.HttpRequest; import org.keycloak.http.HttpRequest;
import org.keycloak.services.resources.Cors; import org.keycloak.services.cors.Cors;
import jakarta.ws.rs.OPTIONS; import jakarta.ws.rs.OPTIONS;
import jakarta.ws.rs.Path; import jakarta.ws.rs.Path;

View file

@ -30,10 +30,10 @@ import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.services.ForbiddenException; import org.keycloak.services.ForbiddenException;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.Cors;
import org.keycloak.services.resources.admin.info.ServerInfoAdminResource; import org.keycloak.services.resources.admin.info.ServerInfoAdminResource;
import org.keycloak.services.resources.admin.permissions.AdminPermissions; import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.theme.Theme; import org.keycloak.theme.Theme;

View file

@ -23,7 +23,7 @@ import jakarta.ws.rs.core.Response;
import org.keycloak.http.HttpRequest; import org.keycloak.http.HttpRequest;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.services.resources.Cors; import org.keycloak.services.cors.Cors;
public class RealmsAdminResourcePreflight extends RealmsAdminResource { public class RealmsAdminResourcePreflight extends RealmsAdminResource {

View file

@ -38,7 +38,7 @@ import jakarta.ws.rs.core.Response;
import org.keycloak.OAuthErrorException; import org.keycloak.OAuthErrorException;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation; import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.services.resources.Cors; import org.keycloak.services.cors.Cors;
import static jakarta.ws.rs.core.HttpHeaders.WWW_AUTHENTICATE; import static jakarta.ws.rs.core.HttpHeaders.WWW_AUTHENTICATE;

View file

@ -0,0 +1,20 @@
#
# Copyright 2024 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
org.keycloak.services.cors.DefaultCorsFactory

View file

@ -7,7 +7,7 @@ import org.apache.http.impl.client.HttpClientBuilder;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.keycloak.services.resources.Cors; import org.keycloak.services.cors.Cors;
import java.io.IOException; import java.io.IOException;

View file

@ -77,7 +77,7 @@ import org.keycloak.representations.oidc.TokenMetadataRepresentation;
import org.keycloak.services.clientpolicy.ClientPolicyException; import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.condition.ClientAccessTypeConditionFactory; import org.keycloak.services.clientpolicy.condition.ClientAccessTypeConditionFactory;
import org.keycloak.services.clientpolicy.executor.DPoPBindEnforcerExecutorFactory; import org.keycloak.services.clientpolicy.executor.DPoPBindEnforcerExecutorFactory;
import org.keycloak.services.resources.Cors; import org.keycloak.services.cors.Cors;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;

View file

@ -41,7 +41,7 @@ import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.clientregistration.ClientRegistrationService; import org.keycloak.services.clientregistration.ClientRegistrationService;
import org.keycloak.services.clientregistration.oidc.OIDCClientRegistrationProviderFactory; import org.keycloak.services.clientregistration.oidc.OIDCClientRegistrationProviderFactory;
import org.keycloak.services.resources.Cors; import org.keycloak.services.cors.Cors;
import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.RealmsResource;
import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.Assert;