Cookie Provider (#26499)
Closes #26500 Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
parent
3655268e4d
commit
bc3c27909e
24 changed files with 376 additions and 98 deletions
|
@ -33,7 +33,9 @@ public class ServerCookie implements Serializable {
|
|||
private static final String tspecials2 = "()<>@,;:\\\"/[]?={} \t";
|
||||
|
||||
public enum SameSiteAttributeValue {
|
||||
NONE("None"); // we currently support only SameSite=None; this might change in the future
|
||||
NONE("None"),
|
||||
LAX("Lax"),
|
||||
STRICT("Strict");
|
||||
|
||||
private final String specValue;
|
||||
SameSiteAttributeValue(String specValue) {
|
||||
|
|
|
@ -170,3 +170,8 @@ link:{upgradingguide_link}[{upgradingguide_name}].
|
|||
= Authorization Policy
|
||||
|
||||
In previous versions of Keycloak when the last member of a User, Group or Client policy was deleted then that policy would also be deleted. Unfortunately this could lead to an escalation of privileges if the policy was used in an aggregate policy. To avoid privilege escalation the effect policies are no longer deleted and an administrator will need to update those policies.
|
||||
|
||||
= Updates to cookies
|
||||
|
||||
Cookie handling code has been refactored and improved, including a new Cookie Provider. This provides better consistency
|
||||
for cookies handled by Keycloak, and the ability to introduce configuration options around cookies if needed.
|
|
@ -285,3 +285,15 @@ After removal of the Map Store the following modules were renamed:
|
|||
* `org.keycloak:keycloak-model-legacy` to `org.keycloak:keycloak-model-storage`
|
||||
* `org.keycloak:keycloak-model-legacy-private` to `org.keycloak:keycloak-model-storage-private`
|
||||
* `org.keycloak:keycloak-model-legacy-services` to `org.keycloak:keycloak-model-storage-services`
|
||||
|
||||
= Updates to cookies
|
||||
|
||||
As part of refactoring cookie handling in Keycloak there are some changes to how cookies are set:
|
||||
|
||||
* All cookies will now have the secure attribute set if the request is through a secure context
|
||||
* KEYCLOAK_LOCALE and WELCOME_STATE_CHECKER cookies now set SameSite=Strict
|
||||
|
||||
For custom extensions there may be some changes needed:
|
||||
|
||||
* LocaleSelectorProvider.KEYCLOAK_LOCALE is deprecated as cookies are now managed through the CookieProvider
|
||||
* HttpResponse.setWriteCookiesOnTransactionComplete has been removed
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package org.keycloak.cookie;
|
||||
|
||||
public interface CookieMaxAge {
|
||||
|
||||
int EXPIRED = 0;
|
||||
|
||||
int SESSION = -1;
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package org.keycloak.cookie;
|
||||
|
||||
enum CookiePath {
|
||||
REALM,
|
||||
REQUEST
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package org.keycloak.cookie;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
public interface CookieProvider extends Provider {
|
||||
|
||||
void set(CookieType cookieType, String value);
|
||||
|
||||
void set(CookieType cookieType, String value, int maxAge);
|
||||
|
||||
String get(CookieType cookieType);
|
||||
|
||||
void expire(CookieType cookieType);
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package org.keycloak.cookie;
|
||||
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
public interface CookieProviderFactory extends ProviderFactory<CookieProvider> {
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package org.keycloak.cookie;
|
||||
|
||||
import org.keycloak.common.util.ServerCookie;
|
||||
|
||||
enum CookieScope {
|
||||
// Internal cookies are only available for direct requests to Keycloak
|
||||
INTERNAL(ServerCookie.SameSiteAttributeValue.STRICT, true),
|
||||
|
||||
// Federation cookies are available after redirect from applications, and are also available in an iframe context
|
||||
// unless the browser blocks third-party cookies
|
||||
FEDERATION(ServerCookie.SameSiteAttributeValue.NONE, true),
|
||||
|
||||
// Federation cookies that are also available from JavaScript
|
||||
FEDERATION_JS(ServerCookie.SameSiteAttributeValue.NONE, false),
|
||||
|
||||
// Legacy cookies do not set the SameSite attribute and will default to SameSite=Lax in modern browsers
|
||||
@Deprecated
|
||||
LEGACY(null, true),
|
||||
|
||||
// Legacy cookies that are also available from JavaScript
|
||||
@Deprecated
|
||||
LEGACY_JS(null, false);
|
||||
|
||||
private final ServerCookie.SameSiteAttributeValue sameSite;
|
||||
private final boolean httpOnly;
|
||||
|
||||
CookieScope(ServerCookie.SameSiteAttributeValue sameSite, boolean httpOnly) {
|
||||
this.sameSite = sameSite;
|
||||
this.httpOnly = httpOnly;
|
||||
}
|
||||
|
||||
public ServerCookie.SameSiteAttributeValue getSameSite() {
|
||||
return sameSite;
|
||||
}
|
||||
|
||||
public boolean isHttpOnly() {
|
||||
return httpOnly;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package org.keycloak.cookie;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
public class CookieSpi implements Spi {
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "cookie";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return CookieProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return CookieProviderFactory.class;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package org.keycloak.cookie;
|
||||
|
||||
public enum CookieType {
|
||||
|
||||
KEYCLOAK_LOCALE(CookiePath.REALM, CookieScope.INTERNAL, CookieMaxAge.SESSION),
|
||||
WELCOME_STATE_CHECKER(CookiePath.REQUEST, CookieScope.INTERNAL, 300),
|
||||
KC_AUTH_STATE(CookiePath.REALM, CookieScope.LEGACY_JS), // TODO Change CookieScope
|
||||
KC_RESTART(CookiePath.REALM, CookieScope.LEGACY, CookieMaxAge.SESSION); // TODO Change CookieScope
|
||||
|
||||
private final CookiePath path;
|
||||
private final CookieScope scope;
|
||||
|
||||
private final Integer defaultMaxAge;
|
||||
|
||||
CookieType(CookiePath path, CookieScope scope) {
|
||||
this.path = path;
|
||||
this.scope = scope;
|
||||
this.defaultMaxAge = null;
|
||||
}
|
||||
|
||||
CookieType(CookiePath path, CookieScope scope, int defaultMaxAge) {
|
||||
this.path = path;
|
||||
this.scope = scope;
|
||||
this.defaultMaxAge = defaultMaxAge;
|
||||
}
|
||||
|
||||
public CookiePath getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public CookieScope getScope() {
|
||||
return scope;
|
||||
}
|
||||
|
||||
public Integer getDefaultMaxAge() {
|
||||
return defaultMaxAge;
|
||||
}
|
||||
|
||||
}
|
|
@ -93,3 +93,4 @@ org.keycloak.services.clientpolicy.ClientPolicyManagerSpi
|
|||
org.keycloak.userprofile.UserProfileSpi
|
||||
org.keycloak.device.DeviceRepresentationSpi
|
||||
org.keycloak.health.LoadBalancerCheckSpi
|
||||
org.keycloak.cookie.CookieSpi
|
|
@ -24,6 +24,7 @@ import java.util.Locale;
|
|||
|
||||
public interface LocaleSelectorProvider extends Provider {
|
||||
|
||||
@Deprecated(since = "24.0.0", forRemoval = true)
|
||||
String LOCALE_COOKIE = "KEYCLOAK_LOCALE";
|
||||
String KC_LOCALE_PARAM = "kc_locale";
|
||||
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
package org.keycloak.cookie;
|
||||
|
||||
import jakarta.ws.rs.core.Cookie;
|
||||
import org.keycloak.common.util.ServerCookie;
|
||||
import org.keycloak.http.HttpCookie;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.urls.UrlType;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
public class DefaultCookieProvider implements CookieProvider {
|
||||
|
||||
private static final String LEGACY_SUFFIX = "_LEGACY";
|
||||
|
||||
private KeycloakSession session;
|
||||
|
||||
private boolean legacyCookiesEnabled;
|
||||
|
||||
public DefaultCookieProvider(KeycloakSession session, boolean legacyCookiesEnabled) {
|
||||
this.session = session;
|
||||
this.legacyCookiesEnabled = legacyCookiesEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(CookieType cookieType, String value) {
|
||||
if (cookieType.getDefaultMaxAge() == null) {
|
||||
throw new IllegalArgumentException(cookieType + " has no default max-age");
|
||||
}
|
||||
|
||||
set(cookieType, value, cookieType.getDefaultMaxAge());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(CookieType cookieType, String value, int maxAge) {
|
||||
String name = cookieType.name();
|
||||
ServerCookie.SameSiteAttributeValue sameSite = cookieType.getScope().getSameSite();
|
||||
boolean secure = resolveSecure(sameSite);
|
||||
String path = resolvePath(cookieType);
|
||||
boolean httpOnly = cookieType.getScope().isHttpOnly();
|
||||
|
||||
HttpCookie newCookie = new HttpCookie(1, name, value, path, null, null, maxAge, secure, httpOnly, sameSite);
|
||||
session.getContext().getHttpResponse().setCookieIfAbsent(newCookie);
|
||||
|
||||
if (legacyCookiesEnabled) {
|
||||
if (ServerCookie.SameSiteAttributeValue.NONE.equals(sameSite)) {
|
||||
String legacyName = name + LEGACY_SUFFIX;
|
||||
HttpCookie legacyCookie = new HttpCookie(1, legacyName, value, path, null, null, maxAge, secure, httpOnly, null);
|
||||
session.getContext().getHttpResponse().setCookieIfAbsent(legacyCookie);
|
||||
}
|
||||
} else {
|
||||
expireLegacy(cookieType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get(CookieType cookieType) {
|
||||
Cookie cookie = session.getContext().getRequestHeaders().getCookies().get(cookieType.name());
|
||||
return cookie != null ? cookie.getValue() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void expire(CookieType cookieType) {
|
||||
Cookie cookie = session.getContext().getRequestHeaders().getCookies().get(cookieType.name());
|
||||
expire(cookie, cookieType);
|
||||
|
||||
expireLegacy(cookieType);
|
||||
}
|
||||
|
||||
private void expireLegacy(CookieType cookieType) {
|
||||
String legacyName = cookieType.name() + LEGACY_SUFFIX;
|
||||
Cookie legacyCookie = session.getContext().getRequestHeaders().getCookies().get(legacyName);
|
||||
expire(legacyCookie, cookieType);
|
||||
}
|
||||
|
||||
private void expire(Cookie cookie, CookieType cookieType) {
|
||||
if (cookie != null) {
|
||||
String path = resolvePath(cookieType);
|
||||
HttpCookie newCookie = new HttpCookie(1, cookie.getName(), "", path, null, null, 0, false, false, null);
|
||||
session.getContext().getHttpResponse().setCookieIfAbsent(newCookie);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
private String resolvePath(CookieType cookieType) {
|
||||
switch (cookieType.getPath()) {
|
||||
case REALM:
|
||||
return RealmsResource.realmBaseUrl(session.getContext().getUri()).path("/").build(session.getContext().getRealm().getName()).getRawPath();
|
||||
case REQUEST:
|
||||
return session.getContext().getUri().getRequestUri().getRawPath();
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported enum value " + cookieType.getPath().name());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean resolveSecure(ServerCookie.SameSiteAttributeValue sameSite) {
|
||||
URI requestUri = session.getContext().getUri().getRequestUri();
|
||||
|
||||
// SameSite=none requires secure context
|
||||
if (ServerCookie.SameSiteAttributeValue.NONE.equals(sameSite)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
if (realm != null && realm.getSslRequired().isRequired(requestUri.getHost())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ("https".equals(requestUri.getScheme())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Browsers consider 127.0.0.1, localhost and *.localhost as secure contexts
|
||||
String frontendHostname = session.getContext().getUri(UrlType.FRONTEND).getRequestUri().getHost();
|
||||
if (frontendHostname.equals("127.0.0.1") || frontendHostname.equals("localhost") || frontendHostname.endsWith(".localhost")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package org.keycloak.cookie;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
||||
public class DefaultCookieProviderFactory implements CookieProviderFactory {
|
||||
|
||||
private boolean legacyCookies;
|
||||
|
||||
@Override
|
||||
public CookieProvider create(KeycloakSession session) {
|
||||
return new DefaultCookieProvider(session, legacyCookies);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
legacyCookies = config.getBoolean("legacyCookies", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "default";
|
||||
}
|
||||
|
||||
}
|
|
@ -26,6 +26,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||
import jakarta.ws.rs.core.UriInfo;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.cookie.CookieProvider;
|
||||
import org.keycloak.cookie.CookieType;
|
||||
import org.keycloak.models.KeycloakContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
@ -67,30 +69,23 @@ public class AuthenticationStateCookie {
|
|||
this.remainingTabs = remainingTabs;
|
||||
}
|
||||
|
||||
public static void generateAndSetCookie(KeycloakSession session, RealmModel realm, RootAuthenticationSessionModel rootAuthSession, int cookieMaxAge) {
|
||||
UriInfo uriInfo = session.getContext().getHttpRequest().getUri();
|
||||
String path = AuthenticationManager.getRealmCookiePath(realm, uriInfo);
|
||||
boolean secureOnly = realm.getSslRequired().isRequired(session.getContext().getConnection());
|
||||
|
||||
public static void generateAndSetCookie(KeycloakSession session, RootAuthenticationSessionModel rootAuthSession, int cookieMaxAge) {
|
||||
AuthenticationStateCookie cookie = new AuthenticationStateCookie();
|
||||
cookie.setAuthSessionId(rootAuthSession.getId());
|
||||
cookie.setRemainingTabs(rootAuthSession.getAuthenticationSessions().keySet());
|
||||
|
||||
try {
|
||||
String encoded = JsonSerialization.writeValueAsString(cookie);
|
||||
logger.tracef("Generating new %s cookie. Cookie: %s, Cookie lifespan: %d", KC_AUTH_STATE, encoded, cookieMaxAge);
|
||||
logger.tracef("Generating new %s cookie. Cookie: %s, Cookie lifespan: %d", CookieType.KC_AUTH_STATE, encoded, cookieMaxAge);
|
||||
|
||||
CookieHelper.addCookie(KC_AUTH_STATE, encoded, path, null, null, cookieMaxAge, secureOnly, false, session);
|
||||
session.getProvider(CookieProvider.class).set(CookieType.KC_AUTH_STATE, encoded, cookieMaxAge);
|
||||
} catch (IOException ioe) {
|
||||
throw new IllegalStateException("Exception thrown when encoding cookie", ioe);
|
||||
}
|
||||
}
|
||||
|
||||
public static void expireCookie(RealmModel realm, KeycloakSession session) {
|
||||
UriInfo uriInfo = session.getContext().getHttpRequest().getUri();
|
||||
String path = AuthenticationManager.getRealmCookiePath(realm, uriInfo);
|
||||
boolean secureOnly = realm.getSslRequired().isRequired(session.getContext().getConnection());
|
||||
CookieHelper.addCookie(KC_AUTH_STATE, "", path, null, null, 0, secureOnly, false, session);
|
||||
public static void expireCookie(KeycloakSession session) {
|
||||
session.getProvider(CookieProvider.class).expire(CookieType.KC_AUTH_STATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -17,12 +17,13 @@
|
|||
package org.keycloak.locale;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.cookie.CookieProvider;
|
||||
import org.keycloak.cookie.CookieType;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
import jakarta.ws.rs.core.Cookie;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
@ -131,12 +132,12 @@ public class DefaultLocaleSelectorProvider implements LocaleSelectorProvider {
|
|||
return null;
|
||||
}
|
||||
|
||||
Cookie localeCookie = httpHeaders.getCookies().get(LOCALE_COOKIE);
|
||||
String localeCookie = session.getProvider(CookieProvider.class).get(CookieType.KEYCLOAK_LOCALE);
|
||||
if (localeCookie == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return findLocale(realm, localeCookie.getValue());
|
||||
return findLocale(realm, localeCookie);
|
||||
}
|
||||
|
||||
private Locale getAcceptLanguageHeaderLocale(RealmModel realm, HttpHeaders httpHeaders) {
|
||||
|
|
|
@ -16,16 +16,14 @@
|
|||
*/
|
||||
package org.keycloak.locale;
|
||||
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.cookie.CookieProvider;
|
||||
import org.keycloak.cookie.CookieType;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.util.CookieHelper;
|
||||
import org.keycloak.storage.ReadOnlyException;
|
||||
|
||||
public class DefaultLocaleUpdaterProvider implements LocaleUpdaterProvider {
|
||||
|
@ -61,21 +59,13 @@ public class DefaultLocaleUpdaterProvider implements LocaleUpdaterProvider {
|
|||
|
||||
@Override
|
||||
public void updateLocaleCookie(String locale) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
UriInfo uriInfo = session.getContext().getUri();
|
||||
|
||||
boolean secure = realm.getSslRequired().isRequired(uriInfo.getRequestUri().getHost());
|
||||
CookieHelper.addCookie(LocaleSelectorProvider.LOCALE_COOKIE, locale, AuthenticationManager.getRealmCookiePath(realm, uriInfo), null, null, -1, secure, true, session);
|
||||
session.getProvider(CookieProvider.class).set(CookieType.KEYCLOAK_LOCALE, locale);
|
||||
logger.debugv("Updating locale cookie to {0}", locale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void expireLocaleCookie() {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
UriInfo uriInfo = session.getContext().getUri();
|
||||
|
||||
boolean secure = realm.getSslRequired().isRequired(session.getContext().getConnection());
|
||||
CookieHelper.addCookie(LocaleSelectorProvider.LOCALE_COOKIE, "", AuthenticationManager.getRealmCookiePath(realm, uriInfo), null, "Expiring cookie", 0, secure, true, session);
|
||||
session.getProvider(CookieProvider.class).expire(CookieType.KEYCLOAK_LOCALE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -17,14 +17,16 @@
|
|||
|
||||
package org.keycloak.protocol;
|
||||
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.http.HttpRequest;
|
||||
import org.keycloak.authentication.AuthenticationProcessor;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.forms.login.LoginFormsProvider;
|
||||
import org.keycloak.http.HttpRequest;
|
||||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -42,9 +44,6 @@ import org.keycloak.services.resources.LoginActionsService;
|
|||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
/**
|
||||
* Common base class for Authorization REST endpoints implementation, which have to be implemented by each protocol.
|
||||
*
|
||||
|
@ -120,7 +119,7 @@ public abstract class AuthorizationEndpointBase {
|
|||
} else {
|
||||
// KEYCLOAK-8043: forward the request with prompt=none to the default provider.
|
||||
if ("true".equals(authSession.getAuthNote(AuthenticationProcessor.FORWARDED_PASSIVE_LOGIN))) {
|
||||
RestartLoginCookie.setRestartCookie(session, realm, clientConnection, session.getContext().getUri(), authSession);
|
||||
RestartLoginCookie.setRestartCookie(session, authSession);
|
||||
if (redirectToAuthentication) {
|
||||
return processor.redirectToFlow();
|
||||
}
|
||||
|
@ -146,7 +145,7 @@ public abstract class AuthorizationEndpointBase {
|
|||
return processor.finishAuthentication(protocol);
|
||||
} else {
|
||||
try {
|
||||
RestartLoginCookie.setRestartCookie(session, realm, clientConnection, session.getContext().getUri(), authSession);
|
||||
RestartLoginCookie.setRestartCookie(session, authSession);
|
||||
if (redirectToAuthentication) {
|
||||
return processor.redirectToFlow();
|
||||
}
|
||||
|
|
|
@ -21,19 +21,15 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Token;
|
||||
import org.keycloak.TokenCategory;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.cookie.CookieProvider;
|
||||
import org.keycloak.cookie.CookieType;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||
import org.keycloak.services.util.CookieHelper;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||
|
||||
import jakarta.ws.rs.core.Cookie;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -120,24 +116,18 @@ public class RestartLoginCookie implements Token {
|
|||
}
|
||||
}
|
||||
|
||||
public static void setRestartCookie(KeycloakSession session, RealmModel realm, ClientConnection connection, UriInfo uriInfo, AuthenticationSessionModel authSession) {
|
||||
public static void setRestartCookie(KeycloakSession session, AuthenticationSessionModel authSession) {
|
||||
RestartLoginCookie restart = new RestartLoginCookie(authSession);
|
||||
String encoded = session.tokens().encode(restart);
|
||||
String path = AuthenticationManager.getRealmCookiePath(realm, uriInfo);
|
||||
boolean secureOnly = realm.getSslRequired().isRequired(connection);
|
||||
CookieHelper.addCookie(KC_RESTART, encoded, path, null, null, -1, secureOnly, true, session);
|
||||
session.getProvider(CookieProvider.class).set(CookieType.KC_RESTART, encoded);
|
||||
}
|
||||
|
||||
public static void expireRestartCookie(RealmModel realm, UriInfo uriInfo, KeycloakSession session) {
|
||||
KeycloakContext context = session.getContext();
|
||||
ClientConnection connection = context.getConnection();
|
||||
String path = AuthenticationManager.getRealmCookiePath(realm, uriInfo);
|
||||
boolean secureOnly = realm.getSslRequired().isRequired(connection);
|
||||
CookieHelper.addCookie(KC_RESTART, "", path, null, null, 0, secureOnly, true, session);
|
||||
public static void expireRestartCookie(KeycloakSession session) {
|
||||
session.getProvider(CookieProvider.class).expire(CookieType.KC_RESTART);
|
||||
}
|
||||
|
||||
public static Cookie getRestartCookie(KeycloakSession session){
|
||||
Cookie cook = session.getContext().getRequestHeaders().getCookies().get(KC_RESTART);
|
||||
public static String getRestartCookie(KeycloakSession session){
|
||||
String cook = session.getProvider(CookieProvider.class).get(CookieType.KC_RESTART);
|
||||
if (cook == null) {
|
||||
logger.debug("KC_RESTART cookie doesn't exist");
|
||||
return null;
|
||||
|
@ -147,10 +137,7 @@ public class RestartLoginCookie implements Token {
|
|||
|
||||
public static AuthenticationSessionModel restartSession(KeycloakSession session, RealmModel realm,
|
||||
RootAuthenticationSessionModel rootSession, String expectedClientId,
|
||||
Cookie cook) throws Exception {
|
||||
|
||||
String encodedCookie = cook.getValue();
|
||||
|
||||
String encodedCookie) throws Exception {
|
||||
RestartLoginCookie cookie = session.tokens().decode(encodedCookie, RestartLoginCookie.class);
|
||||
if (cookie == null) {
|
||||
logger.debug("Failed to verify encoded RestartLoginCookie");
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package org.keycloak.services.managers;
|
||||
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.ServerCookie.SameSiteAttributeValue;
|
||||
import org.keycloak.common.util.Time;
|
||||
|
@ -33,13 +34,6 @@ import org.keycloak.sessions.AuthenticationSessionModel;
|
|||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||
import org.keycloak.sessions.StickySessionEncoderProvider;
|
||||
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -191,9 +185,8 @@ public class AuthenticationSessionManager {
|
|||
|
||||
// expire restart cookie
|
||||
if (expireRestartCookie) {
|
||||
UriInfo uriInfo = session.getContext().getUri();
|
||||
RestartLoginCookie.expireRestartCookie(realm, uriInfo, session);
|
||||
AuthenticationStateCookie.expireCookie(realm, session);
|
||||
RestartLoginCookie.expireRestartCookie(session);
|
||||
AuthenticationStateCookie.expireCookie(session);
|
||||
|
||||
// With browser session, this makes sure that info/error pages will be rendered correctly when locale is changed on them
|
||||
session.getProvider(LoginFormsProvider.class).setDetachedAuthSession();
|
||||
|
@ -242,7 +235,7 @@ public class AuthenticationSessionManager {
|
|||
|
||||
log.tracef("Removed authentication session of root session '%s' with tabId '%s'. But there are remaining tabs in the root session. Root authentication session will expire in %d seconds", rootAuthSession.getId(), authSession.getTabId(), authSessionExpiresIn);
|
||||
|
||||
AuthenticationStateCookie.generateAndSetCookie(session, realm, rootAuthSession, authSessionExpiresIn);
|
||||
AuthenticationStateCookie.generateAndSetCookie(session, rootAuthSession, authSessionExpiresIn);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -385,7 +385,7 @@ public class SessionCodeChecks {
|
|||
logger.debug("Authentication session not found. Trying to restart from cookie.");
|
||||
AuthenticationSessionModel authSession = null;
|
||||
|
||||
Cookie cook = RestartLoginCookie.getRestartCookie(session);
|
||||
String cook = RestartLoginCookie.getRestartCookie(session);
|
||||
if (cook == null) {
|
||||
event.error(Errors.COOKIE_NOT_FOUND);
|
||||
return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.COOKIE_NOT_FOUND);
|
||||
|
|
|
@ -24,7 +24,6 @@ import jakarta.ws.rs.PathParam;
|
|||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.Cookie;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
@ -37,13 +36,14 @@ import org.keycloak.common.Version;
|
|||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.MimeTypeUtil;
|
||||
import org.keycloak.common.util.SecretGenerator;
|
||||
import org.keycloak.cookie.CookieProvider;
|
||||
import org.keycloak.cookie.CookieType;
|
||||
import org.keycloak.http.HttpRequest;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.services.ForbiddenException;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.managers.ApplianceBootstrap;
|
||||
import org.keycloak.services.util.CacheControlUtil;
|
||||
import org.keycloak.services.util.CookieHelper;
|
||||
import org.keycloak.theme.Theme;
|
||||
import org.keycloak.theme.freemarker.FreeMarkerProvider;
|
||||
import org.keycloak.urls.UrlType;
|
||||
|
@ -283,28 +283,17 @@ public class WelcomeResource {
|
|||
|
||||
private String setCsrfCookie() {
|
||||
String stateChecker = Base64Url.encode(SecretGenerator.getInstance().randomBytes());
|
||||
String cookiePath = session.getContext().getUri().getPath();
|
||||
boolean secureOnly = session.getContext().getUri().getRequestUri().getScheme().equalsIgnoreCase("https");
|
||||
CookieHelper.addCookie(KEYCLOAK_STATE_CHECKER, stateChecker, cookiePath, null, null, 300, secureOnly, true, session);
|
||||
session.getProvider(CookieProvider.class).set(CookieType.WELCOME_STATE_CHECKER, stateChecker);
|
||||
return stateChecker;
|
||||
}
|
||||
|
||||
private void expireCsrfCookie() {
|
||||
String cookiePath = session.getContext().getUri().getPath();
|
||||
boolean secureOnly = session.getContext().getUri().getRequestUri().getScheme().equalsIgnoreCase("https");
|
||||
CookieHelper.addCookie(KEYCLOAK_STATE_CHECKER, "", cookiePath, null, null, 0, secureOnly, true, session);
|
||||
session.getProvider(CookieProvider.class).expire(CookieType.WELCOME_STATE_CHECKER);
|
||||
}
|
||||
|
||||
private void csrfCheck(final MultivaluedMap<String, String> formData) {
|
||||
String formStateChecker = formData.getFirst("stateChecker");
|
||||
HttpRequest request = session.getContext().getHttpRequest();
|
||||
HttpHeaders headers = request.getHttpHeaders();
|
||||
Cookie cookie = headers.getCookies().get(KEYCLOAK_STATE_CHECKER);
|
||||
if (cookie == null) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
String cookieStateChecker = cookie.getValue();
|
||||
String cookieStateChecker = session.getProvider(CookieProvider.class).get(CookieType.WELCOME_STATE_CHECKER);
|
||||
|
||||
if (cookieStateChecker == null || !cookieStateChecker.equals(formStateChecker)) {
|
||||
throw new ForbiddenException();
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.cookie.DefaultCookieProviderFactory
|
|
@ -16,13 +16,9 @@
|
|||
*/
|
||||
package org.keycloak.testsuite.i18n;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
|
||||
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
|
||||
import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient43Engine;
|
||||
|
@ -33,6 +29,8 @@ import org.keycloak.OAuth2Constants;
|
|||
import org.keycloak.adapters.HttpClientBuilder;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||
import org.keycloak.cookie.CookieType;
|
||||
import org.keycloak.cookie.DefaultCookieProvider;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.forms.login.freemarker.DetachedInfoStateChecker;
|
||||
|
@ -46,18 +44,21 @@ import org.keycloak.testsuite.pages.AppPage;
|
|||
import org.keycloak.testsuite.pages.ErrorPage;
|
||||
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
|
||||
import org.keycloak.testsuite.pages.OAuthGrantPage;
|
||||
import org.keycloak.testsuite.util.IdentityProviderBuilder;
|
||||
import org.openqa.selenium.Cookie;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:gerbermichi@me.com">Michael Gerber</a>
|
||||
|
@ -231,7 +232,7 @@ public class LoginPageTest extends AbstractI18NTest {
|
|||
|
||||
assertEquals("Deutsch", loginPage.getLanguageDropdownText());
|
||||
|
||||
Cookie localeCookie = driver.manage().getCookieNamed(LocaleSelectorProvider.LOCALE_COOKIE);
|
||||
Cookie localeCookie = driver.manage().getCookieNamed(CookieType.KEYCLOAK_LOCALE.name());
|
||||
assertEquals("de", localeCookie.getValue());
|
||||
|
||||
UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost");
|
||||
|
@ -276,7 +277,7 @@ public class LoginPageTest extends AbstractI18NTest {
|
|||
loginPage.open();
|
||||
|
||||
// Cookie should be removed as last user to login didn't have a locale
|
||||
localeCookie = driver.manage().getCookieNamed(LocaleSelectorProvider.LOCALE_COOKIE);
|
||||
localeCookie = driver.manage().getCookieNamed(CookieType.KEYCLOAK_LOCALE.name());
|
||||
Assert.assertNull(localeCookie);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue