Use code from RestEasy to create and set cookies (#26558)

Closes #26557

Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
Stian Thorgersen 2024-02-06 15:14:04 +01:00 committed by GitHub
parent acd9def8aa
commit c4b1fd092a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 304 additions and 139 deletions

View file

@ -27,7 +27,11 @@ import java.util.TimeZone;
/** /**
* Server-side cookie representation. borrowed from Tomcat. * Server-side cookie representation. borrowed from Tomcat.
*
* @deprecated Should not be used on the Keycloak server-side, or in extensions. Will be removed when no longer used by
* adapters
*/ */
@Deprecated
public class ServerCookie implements Serializable { public class ServerCookie implements Serializable {
private static final String tspecials = ",; "; private static final String tspecials = ",; ";
private static final String tspecials2 = "()<>@,;:\\\"/[]?={} \t"; private static final String tspecials2 = "()<>@,;:\\\"/[]?={} \t";

View file

@ -306,9 +306,11 @@ and `org.keycloak:keycloak-model-legacy` module was deprecated and will be remov
As part of refactoring cookie handling in Keycloak there are some changes to how cookies are set: 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 * 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 * `WELCOME_STATE_CHECKER` cookies now set `SameSite=Strict`
For custom extensions there may be some changes needed: For custom extensions there may be some changes needed:
* LocaleSelectorProvider.KEYCLOAK_LOCALE is deprecated as cookies are now managed through the CookieProvider * `LocaleSelectorProvider.KEYCLOAK_LOCALE` is deprecated as cookies are now managed through the CookieProvider
* HttpResponse.setWriteCookiesOnTransactionComplete has been removed * `HttpResponse.setWriteCookiesOnTransactionComplete` has been removed
* `HttpCookie` is deprecated, please use `NewCookie.Builder` instead
* `ServerCookie` is deprecated, please use `NewCookie.Builder` instead

View file

@ -112,7 +112,7 @@
<jboss.spec.javax.servlet.jsp.jboss-jsp-api_2.3_spec.version>2.0.0.Final</jboss.spec.javax.servlet.jsp.jboss-jsp-api_2.3_spec.version> <jboss.spec.javax.servlet.jsp.jboss-jsp-api_2.3_spec.version>2.0.0.Final</jboss.spec.javax.servlet.jsp.jboss-jsp-api_2.3_spec.version>
<log4j.version>1.2.17</log4j.version> <log4j.version>1.2.17</log4j.version>
<resteasy-legacy.version>4.7.7.Final</resteasy-legacy.version> <resteasy-legacy.version>4.7.7.Final</resteasy-legacy.version>
<resteasy.version>6.2.4.Final</resteasy.version> <resteasy.version>6.2.7.Final</resteasy.version>
<resteasy.undertow.version>${resteasy.version}</resteasy.undertow.version> <resteasy.undertow.version>${resteasy.version}</resteasy.undertow.version>
<owasp.html.sanitizer.version>20220608.1</owasp.html.sanitizer.version> <owasp.html.sanitizer.version>20220608.1</owasp.html.sanitizer.version>
<slf4j.version>2.0.6</slf4j.version> <slf4j.version>2.0.6</slf4j.version>

View file

@ -18,9 +18,10 @@
package org.keycloak.quarkus.runtime.integration.resteasy; package org.keycloak.quarkus.runtime.integration.resteasy;
import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.NewCookie;
import jakarta.ws.rs.ext.RuntimeDelegate;
import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
import org.jboss.resteasy.reactive.server.vertx.VertxResteasyReactiveRequestContext; import org.jboss.resteasy.reactive.server.vertx.VertxResteasyReactiveRequestContext;
import org.keycloak.http.HttpCookie;
import org.keycloak.http.HttpResponse; import org.keycloak.http.HttpResponse;
import java.util.HashSet; import java.util.HashSet;
@ -28,9 +29,11 @@ import java.util.Set;
public final class QuarkusHttpResponse implements HttpResponse { public final class QuarkusHttpResponse implements HttpResponse {
private static final RuntimeDelegate.HeaderDelegate<NewCookie> NEW_COOKIE_HEADER_DELEGATE = RuntimeDelegate.getInstance().createHeaderDelegate(NewCookie.class);
private final ResteasyReactiveRequestContext requestContext; private final ResteasyReactiveRequestContext requestContext;
private Set<HttpCookie> cookies; private Set<NewCookie> newCookies;
public QuarkusHttpResponse(ResteasyReactiveRequestContext requestContext) { public QuarkusHttpResponse(ResteasyReactiveRequestContext requestContext) {
this.requestContext = requestContext; this.requestContext = requestContext;
@ -58,18 +61,18 @@ public final class QuarkusHttpResponse implements HttpResponse {
} }
@Override @Override
public void setCookieIfAbsent(HttpCookie cookie) { public void setCookieIfAbsent(NewCookie newCookie) {
if (cookie == null) { if (newCookie == null) {
throw new IllegalArgumentException("Cookie is null"); throw new IllegalArgumentException("Cookie is null");
} }
if (cookies == null) { if (newCookies == null) {
cookies = new HashSet<>(); newCookies = new HashSet<>();
} }
if (cookies.add(cookie)) { if (newCookies.add(newCookie)) {
addHeader(HttpHeaders.SET_COOKIE, cookie.toHeaderValue()); String headerValue = NEW_COOKIE_HEADER_DELEGATE.toString(newCookie);
addHeader(HttpHeaders.SET_COOKIE, headerValue);
} }
} }
} }

View file

@ -1,17 +1,17 @@
package org.keycloak.cookie; package org.keycloak.cookie;
import org.keycloak.common.util.ServerCookie; import jakarta.ws.rs.core.NewCookie;
public enum CookieScope { public enum CookieScope {
// Internal cookies are only available for direct requests to Keycloak // Internal cookies are only available for direct requests to Keycloak
INTERNAL(ServerCookie.SameSiteAttributeValue.STRICT, true), INTERNAL(NewCookie.SameSite.STRICT, true),
// Federation cookies are available after redirect from applications, and are also available in an iframe context // Federation cookies are available after redirect from applications, and are also available in an iframe context
// unless the browser blocks third-party cookies // unless the browser blocks third-party cookies
FEDERATION(ServerCookie.SameSiteAttributeValue.NONE, true), FEDERATION(NewCookie.SameSite.NONE, true),
// Federation cookies that are also available from JavaScript // Federation cookies that are also available from JavaScript
FEDERATION_JS(ServerCookie.SameSiteAttributeValue.NONE, false), FEDERATION_JS(NewCookie.SameSite.NONE, false),
// Legacy cookies do not set the SameSite attribute and will default to SameSite=Lax in modern browsers // Legacy cookies do not set the SameSite attribute and will default to SameSite=Lax in modern browsers
@Deprecated @Deprecated
@ -21,15 +21,15 @@ public enum CookieScope {
@Deprecated @Deprecated
LEGACY_JS(null, false); LEGACY_JS(null, false);
private final ServerCookie.SameSiteAttributeValue sameSite; private final NewCookie.SameSite sameSite;
private final boolean httpOnly; private final boolean httpOnly;
CookieScope(ServerCookie.SameSiteAttributeValue sameSite, boolean httpOnly) { CookieScope(NewCookie.SameSite sameSite, boolean httpOnly) {
this.sameSite = sameSite; this.sameSite = sameSite;
this.httpOnly = httpOnly; this.httpOnly = httpOnly;
} }
public ServerCookie.SameSiteAttributeValue getSameSite() { public NewCookie.SameSite getSameSite() {
return sameSite; return sameSite;
} }

View file

@ -17,33 +17,36 @@
package org.keycloak.http; package org.keycloak.http;
import org.keycloak.common.util.ServerCookie; import jakarta.ws.rs.core.NewCookie;
import jakarta.ws.rs.ext.RuntimeDelegate;
import org.keycloak.common.util.ServerCookie.SameSiteAttributeValue; import org.keycloak.common.util.ServerCookie.SameSiteAttributeValue;
/** /**
* An extension of {@link javax.ws.rs.core.Cookie} in order to support additional * An extension of {@link javax.ws.rs.core.Cookie} in order to support additional
* fields and behavior. * fields and behavior.
*
* @deprecated This class will be removed in the future. Please use {@link jakarta.ws.rs.core.NewCookie.Builder}
*/ */
public final class HttpCookie extends jakarta.ws.rs.core.Cookie { @Deprecated(since = "24.0.0", forRemoval = true)
public final class HttpCookie extends NewCookie {
private final String comment;
private final int maxAge;
private final boolean secure;
private final boolean httpOnly;
private final SameSiteAttributeValue sameSite;
public HttpCookie(int version, String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly, SameSiteAttributeValue sameSite) { public HttpCookie(int version, String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly, SameSiteAttributeValue sameSite) {
super(name, value, path, domain, version); super(name, value, path, domain, version, comment, maxAge, null, secure, httpOnly, convertSameSite(sameSite));
this.comment = comment; }
this.maxAge = maxAge;
this.secure = secure; private static SameSite convertSameSite(SameSiteAttributeValue sameSiteAttributeValue) {
this.httpOnly = httpOnly; if (sameSiteAttributeValue == null) {
this.sameSite = sameSite; return null;
}
switch (sameSiteAttributeValue) {
case NONE: return SameSite.NONE;
case LAX: return SameSite.LAX;
case STRICT: return SameSite.STRICT;
}
throw new IllegalArgumentException("Unknown SameSite value " + sameSiteAttributeValue);
} }
public String toHeaderValue() { public String toHeaderValue() {
StringBuilder cookieBuf = new StringBuilder(); return RuntimeDelegate.getInstance().createHeaderDelegate(NewCookie.class).toString(this);
ServerCookie.appendCookieValue(cookieBuf, getVersion(), getName(), getValue(), getPath(), getDomain(), comment, maxAge, secure, httpOnly, sameSite);
return cookieBuf.toString();
} }
} }

View file

@ -17,6 +17,8 @@
package org.keycloak.http; package org.keycloak.http;
import jakarta.ws.rs.core.NewCookie;
/** /**
* <p>Represents an out coming HTTP response. * <p>Represents an out coming HTTP response.
* *
@ -56,6 +58,17 @@ public interface HttpResponse {
* *
* @param cookie the cookie * @param cookie the cookie
*/ */
void setCookieIfAbsent(HttpCookie cookie); void setCookieIfAbsent(NewCookie cookie);
/**
* Sets a new cookie only if not yet set.
* @deprecated This method will be removed in the future. Please use {@link jakarta.ws.rs.core.NewCookie.Builder}
*
* @param cookie the cookie
*/
@Deprecated(since = "24.0.0", forRemoval = true)
default void setCookieIfAbsent(HttpCookie cookie) {
setCookieIfAbsent((NewCookie) cookie);
}
} }

View file

@ -0,0 +1,34 @@
package org.keycloak.cookie;
import org.keycloak.models.KeycloakContext;
import org.keycloak.services.resources.RealmsResource;
class CookiePathResolver {
private final KeycloakContext context;
private String realmPath;
private String requestPath;
CookiePathResolver(KeycloakContext context) {
this.context = context;
}
String resolvePath(CookieType cookieType) {
switch (cookieType.getPath()) {
case REALM:
if (realmPath == null) {
realmPath = RealmsResource.realmBaseUrl(context.getUri()).path("/").build(context.getRealm().getName()).getRawPath();
}
return realmPath;
case REQUEST:
if (requestPath == null) {
requestPath = context.getUri().getRequestUri().getRawPath();
}
return requestPath;
default:
throw new IllegalArgumentException("Unsupported enum value " + cookieType.getPath().name());
}
}
}

View file

@ -0,0 +1,39 @@
package org.keycloak.cookie;
import jakarta.ws.rs.core.NewCookie;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.RealmModel;
import java.net.URI;
class CookieSecureResolver {
private final KeycloakContext context;
private final boolean sameSiteLegacyEnabled;
CookieSecureResolver(KeycloakContext context, boolean sameSiteLegacyEnabled) {
this.context = context;
this.sameSiteLegacyEnabled = sameSiteLegacyEnabled;
}
boolean resolveSecure(NewCookie.SameSite sameSite) {
// Due to cookies with SameSite=none secure context is always required when same-site legacy cookies are disabled
if (!sameSiteLegacyEnabled) {
return true;
} else {
// SameSite=none requires secure context
if (NewCookie.SameSite.NONE.equals(sameSite)) {
return true;
}
URI requestUri = context.getUri().getRequestUri();
RealmModel realm = context.getRealm();
if (realm != null && realm.getSslRequired().isRequired(requestUri.getHost())) {
return true;
}
return false;
}
}
}

View file

@ -1,31 +1,36 @@
package org.keycloak.cookie; package org.keycloak.cookie;
import jakarta.ws.rs.core.Cookie; import jakarta.ws.rs.core.Cookie;
import jakarta.ws.rs.core.NewCookie;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.ServerCookie;
import org.keycloak.http.HttpCookie;
import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakContext;
import org.keycloak.models.RealmModel;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.urls.UrlType;
import java.net.URI;
import java.util.Map; import java.util.Map;
public class DefaultCookieProvider implements CookieProvider { public class DefaultCookieProvider implements CookieProvider {
private static final Logger logger = Logger.getLogger(DefaultCookieProvider.class); private static final Logger logger = Logger.getLogger(DefaultCookieProvider.class);
private final KeycloakContext context; private final KeycloakContext context;
private CookiePathResolver pathResolver;
private CookieSecureResolver secureResolver;
private final Map<String, Cookie> cookies; private final Map<String, Cookie> cookies;
private final boolean legacyCookiesEnabled; private final boolean sameSiteLegacyEnabled;
public DefaultCookieProvider(KeycloakContext context, boolean legacyCookiesEnabled) { public DefaultCookieProvider(KeycloakContext context, boolean sameSiteLegacyEnabled) {
this.context = context; this.context = context;
this.cookies = context.getRequestHeaders().getCookies(); this.cookies = context.getRequestHeaders().getCookies();
this.legacyCookiesEnabled = legacyCookiesEnabled; this.pathResolver = new CookiePathResolver(context);
this.secureResolver = new CookieSecureResolver(context, sameSiteLegacyEnabled);
this.sameSiteLegacyEnabled = sameSiteLegacyEnabled;
if (logger.isTraceEnabled()) {
String cookieNames = String.join(", ", this.cookies.keySet());
logger.tracef("Path: %s, cookies: %s", context.getUri().getRequestUri().getRawPath(), cookieNames);
}
} }
@Override @Override
@ -40,59 +45,90 @@ public class DefaultCookieProvider implements CookieProvider {
@Override @Override
public void set(CookieType cookieType, String value, int maxAge) { public void set(CookieType cookieType, String value, int maxAge) {
String name = cookieType.getName(); String name = cookieType.getName();
ServerCookie.SameSiteAttributeValue sameSite = cookieType.getScope().getSameSite(); NewCookie.SameSite sameSite = cookieType.getScope().getSameSite();
boolean secure = resolveSecure(sameSite); boolean secure = secureResolver.resolveSecure(sameSite);
String path = resolvePath(cookieType); String path = pathResolver.resolvePath(cookieType);
boolean httpOnly = cookieType.getScope().isHttpOnly(); boolean httpOnly = cookieType.getScope().isHttpOnly();
HttpCookie newCookie = new HttpCookie(1, name, value, path, null, null, maxAge, secure, httpOnly, sameSite); NewCookie newCookie = new NewCookie.Builder(name)
.version(1)
.value(value)
.path(path)
.maxAge(maxAge)
.secure(secure)
.httpOnly(httpOnly)
.sameSite(sameSite)
.build();
context.getHttpResponse().setCookieIfAbsent(newCookie); context.getHttpResponse().setCookieIfAbsent(newCookie);
logger.tracef("Setting cookie: name: %s, path: %s, same-site: %s, secure: %s, http-only: %s, max-age: %d", name, path, sameSite, secure, httpOnly, maxAge); logger.tracef("Setting cookie: name: %s, path: %s, same-site: %s, secure: %s, http-only: %s, max-age: %d", name, path, sameSite, secure, httpOnly, maxAge);
if (legacyCookiesEnabled && cookieType.supportsSameSiteLegacy()) { setSameSiteLegacy(cookieType, value, maxAge);
if (ServerCookie.SameSiteAttributeValue.NONE.equals(sameSite)) { }
secure = resolveSecure(null);
String legacyName = cookieType.getSameSiteLegacyName();
HttpCookie legacyCookie = new HttpCookie(1, legacyName, value, path, null, null, maxAge, secure, httpOnly, null);
context.getHttpResponse().setCookieIfAbsent(legacyCookie);
logger.tracef("Setting legacy cookie: name: %s, path: %s, same-site: %s, secure: %s, http-only: %s, max-age: %d", legacyName, path, sameSite, secure, httpOnly, maxAge); private void setSameSiteLegacy(CookieType cookieType, String value, int maxAge) {
} if (sameSiteLegacyEnabled && cookieType.supportsSameSiteLegacy()) {
} else { String legacyName = cookieType.getSameSiteLegacyName();
expireLegacy(cookieType); boolean legacySecure = secureResolver.resolveSecure(null);
String path = pathResolver.resolvePath(cookieType);
boolean httpOnly = cookieType.getScope().isHttpOnly();
NewCookie legacyCookie = new NewCookie.Builder(legacyName)
.version(1)
.value(value)
.maxAge(maxAge)
.path(path)
.secure(legacySecure)
.httpOnly(httpOnly)
.build();
context.getHttpResponse().setCookieIfAbsent(legacyCookie);
logger.tracef("Setting legacy cookie: name: %s, path: %s, same-site: %s, secure: %s, http-only: %s, max-age: %d", legacyName, path, null, legacySecure, httpOnly, maxAge);
} else if (cookieType.supportsSameSiteLegacy()) {
expireSameSiteLegacy(cookieType);
} }
} }
@Override @Override
public String get(CookieType cookieType) { public String get(CookieType cookieType) {
Cookie cookie = cookies.get(cookieType.getName()); Cookie cookie = cookies.get(cookieType.getName());
if (cookie == null && cookieType.supportsSameSiteLegacy()) { if (cookie == null) {
cookie = cookies.get(cookieType.getSameSiteLegacyName()); cookie = getSameSiteLegacyCookie(cookieType);
} }
return cookie != null ? cookie.getValue() : null; return cookie != null ? cookie.getValue() : null;
} }
@Override private Cookie getSameSiteLegacyCookie(CookieType cookieType) {
public void expire(CookieType cookieType) {
Cookie cookie = cookies.get(cookieType.getName());
expire(cookie, cookieType);
expireLegacy(cookieType);
}
private void expireLegacy(CookieType cookieType) {
if (cookieType.supportsSameSiteLegacy()) { if (cookieType.supportsSameSiteLegacy()) {
String legacyName = cookieType.getSameSiteLegacyName(); return cookies.get(cookieType.getSameSiteLegacyName());
Cookie legacyCookie = cookies.get(legacyName); } else {
expire(legacyCookie, cookieType); return null;
} }
} }
private void expire(Cookie cookie, CookieType cookieType) { @Override
public void expire(CookieType cookieType) {
expire(cookieType.getName(), cookieType);
expireSameSiteLegacy(cookieType);
}
private void expireSameSiteLegacy(CookieType cookieType) {
if (cookieType.supportsSameSiteLegacy()) {
expire(cookieType.getSameSiteLegacyName(), cookieType);
}
}
private void expire(String cookieName, CookieType cookieType) {
Cookie cookie = cookies.get(cookieName);
if (cookie != null) { if (cookie != null) {
String path = resolvePath(cookieType); String path = pathResolver.resolvePath(cookieType);
HttpCookie newCookie = new HttpCookie(1, cookie.getName(), "", path, null, null, CookieMaxAge.EXPIRED, false, false, null); NewCookie newCookie = new NewCookie.Builder(cookieName)
.version(1)
.path(path)
.maxAge(CookieMaxAge.EXPIRED)
.build();
context.getHttpResponse().setCookieIfAbsent(newCookie); context.getHttpResponse().setCookieIfAbsent(newCookie);
logger.tracef("Expiring cookie: name: %s, path: %s", cookie.getName(), path); logger.tracef("Expiring cookie: name: %s, path: %s", cookie.getName(), path);
@ -103,31 +139,4 @@ public class DefaultCookieProvider implements CookieProvider {
public void close() { public void close() {
} }
private String resolvePath(CookieType cookieType) {
switch (cookieType.getPath()) {
case REALM:
return RealmsResource.realmBaseUrl(context.getUri()).path("/").build(context.getRealm().getName()).getRawPath();
case REQUEST:
return context.getUri().getRequestUri().getRawPath();
default:
throw new IllegalArgumentException("Unsupported enum value " + cookieType.getPath().name());
}
}
private boolean resolveSecure(ServerCookie.SameSiteAttributeValue sameSite) {
URI requestUri = context.getUri().getRequestUri();
// SameSite=none requires secure context
if (ServerCookie.SameSiteAttributeValue.NONE.equals(sameSite)) {
return true;
}
RealmModel realm = context.getRealm();
if (realm != null && realm.getSslRequired().isRequired(requestUri.getHost())) {
return true;
}
return false;
}
} }

View file

@ -3,19 +3,24 @@ package org.keycloak.cookie;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import java.util.List;
public class DefaultCookieProviderFactory implements CookieProviderFactory { public class DefaultCookieProviderFactory implements CookieProviderFactory {
private boolean legacyCookies; private static final String SAME_SITE_LEGACY_KEY = "sameSiteLegacy";
private boolean sameSiteLegacyEnabled;
@Override @Override
public CookieProvider create(KeycloakSession session) { public CookieProvider create(KeycloakSession session) {
return new DefaultCookieProvider(session.getContext(), legacyCookies); return new DefaultCookieProvider(session.getContext(), sameSiteLegacyEnabled);
} }
@Override @Override
public void init(Config.Scope config) { public void init(Config.Scope config) {
legacyCookies = config.getBoolean("legacyCookies", true); sameSiteLegacyEnabled = config.getBoolean(SAME_SITE_LEGACY_KEY, true);
} }
@Override @Override
@ -31,4 +36,15 @@ public class DefaultCookieProviderFactory implements CookieProviderFactory {
return "default"; return "default";
} }
@Override
public List<ProviderConfigProperty> getConfigMetadata() {
return ProviderConfigurationBuilder.create()
.property()
.name(SAME_SITE_LEGACY_KEY)
.type("boolean")
.helpText("Adds legacy cookies without SameSite parameter")
.defaultValue(true)
.add()
.build();
}
} }

View file

@ -24,6 +24,7 @@ import java.util.Set;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.Encode;
import org.keycloak.cookie.CookieProvider; import org.keycloak.cookie.CookieProvider;
import org.keycloak.cookie.CookieType; import org.keycloak.cookie.CookieType;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -69,7 +70,7 @@ public class AuthenticationStateCookie {
cookie.setRemainingTabs(rootAuthSession.getAuthenticationSessions().keySet()); cookie.setRemainingTabs(rootAuthSession.getAuthenticationSessions().keySet());
try { try {
String encoded = JsonSerialization.writeValueAsString(cookie); String encoded = Encode.urlEncode(JsonSerialization.writeValueAsString(cookie));
session.getProvider(CookieProvider.class).set(CookieType.AUTH_STATE, encoded, cookieMaxAge); session.getProvider(CookieProvider.class).set(CookieType.AUTH_STATE, encoded, cookieMaxAge);
} catch (IOException ioe) { } catch (IOException ioe) {
throw new IllegalStateException("Exception thrown when encoding cookie", ioe); throw new IllegalStateException("Exception thrown when encoding cookie", ioe);

View file

@ -17,8 +17,7 @@
package org.keycloak.services; package org.keycloak.services;
import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.NewCookie;
import org.keycloak.http.HttpCookie;
import org.keycloak.http.HttpResponse; import org.keycloak.http.HttpResponse;
import java.util.HashSet; import java.util.HashSet;
@ -27,7 +26,7 @@ import java.util.Set;
public class HttpResponseImpl implements HttpResponse { public class HttpResponseImpl implements HttpResponse {
private final org.jboss.resteasy.spi.HttpResponse delegate; private final org.jboss.resteasy.spi.HttpResponse delegate;
private Set<HttpCookie> cookies; private Set<NewCookie> newCookies;
public HttpResponseImpl(org.jboss.resteasy.spi.HttpResponse delegate) { public HttpResponseImpl(org.jboss.resteasy.spi.HttpResponse delegate) {
this.delegate = delegate; this.delegate = delegate;
@ -54,17 +53,17 @@ public class HttpResponseImpl implements HttpResponse {
} }
@Override @Override
public void setCookieIfAbsent(HttpCookie cookie) { public void setCookieIfAbsent(NewCookie newCookie) {
if (cookie == null) { if (newCookie == null) {
throw new IllegalArgumentException("Cookie is null"); throw new IllegalArgumentException("Cookie is null");
} }
if (cookies == null) { if (newCookies == null) {
cookies = new HashSet<>(); newCookies = new HashSet<>();
} }
if (cookies.add(cookie)) { if (newCookies.add(newCookie)) {
addHeader(HttpHeaders.SET_COOKIE, cookie.toHeaderValue()); delegate.addNewCookie(newCookie);
} }
} }

View file

@ -17,9 +17,7 @@
package org.keycloak.services.managers; package org.keycloak.services.managers;
import jakarta.ws.rs.core.UriInfo;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.ServerCookie.SameSiteAttributeValue;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.cookie.CookieProvider; import org.keycloak.cookie.CookieProvider;
import org.keycloak.cookie.CookieType; import org.keycloak.cookie.CookieType;

View file

@ -109,14 +109,13 @@
} }
function getCookieByName(name) { function getCookieByName(name) {
const cookies = new Map();
for (const cookie of document.cookie.split(";")) { for (const cookie of document.cookie.split(";")) {
const [key, value] = cookie.split("=").map((value) => value.trim()); const [key, value] = cookie.split("=").map((value) => value.trim());
cookies.set(key, value); if (key === name) {
return value.startsWith('"') && value.endsWith('"') ? value.slice(1, -1) : value;
}
} }
return null;
return cookies.get(name) ?? null;
} }
</script> </script>
</body> </body>

View file

@ -19,7 +19,6 @@ import org.keycloak.testsuite.client.KeycloakTestingClient;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
public class DefaultCookieProviderTest extends AbstractKeycloakTest { public class DefaultCookieProviderTest extends AbstractKeycloakTest {
@ -64,6 +63,32 @@ public class DefaultCookieProviderTest extends AbstractKeycloakTest {
assertCookie(response, "WELCOME_STATE_CHECKER", "my-welcome-csrf", "/auth/realms/master/testing/run-on-server", 300, false, true, "Strict", false); assertCookie(response, "WELCOME_STATE_CHECKER", "my-welcome-csrf", "/auth/realms/master/testing/run-on-server", 300, false, true, "Strict", false);
} }
@Test
public void testSessionCookieValue() {
// Set cookie value with '/' that results in cookies value being quoted
final String sessionValue = "my-realm/5256327f-049f-4d01-acb9-68c7936bdeb3/c6cd1f10-40ab-44c0-b77b-6028350d8564";
Response response = testing.server("master").runWithResponse(session -> {
CookieProvider cookies = session.getProvider(CookieProvider.class);
cookies.set(CookieType.SESSION, sessionValue, 444);
});
// Cookie values from response.getCookies() removes quotes
Assert.assertEquals(sessionValue, response.getCookies().get(CookieType.SESSION.getName()).getValue());
// Cookie values from the Set-Cookie header includes quotes
String setHeader = getSetHeader(response, CookieType.SESSION.getName());
Assert.assertTrue(setHeader.startsWith(CookieType.SESSION.getName() + "=\"" + sessionValue + "\";"));
// Send Cookie header with quoted value for cookie
filter.setHeader("Cookie", "KEYCLOAK_SESSION=\"" + sessionValue + "\";");
// Check cookie value matches original value without quotes
testing.server().run(session -> {
String cookieValue = session.getProvider(CookieProvider.class).get(CookieType.SESSION);
Assert.assertEquals(sessionValue, cookieValue);
});
}
@Test @Test
public void testExpire() { public void testExpire() {
filter.setHeader("Cookie", "AUTH_SESSION_ID=new;KC_RESTART=new;"); filter.setHeader("Cookie", "AUTH_SESSION_ID=new;KC_RESTART=new;");
@ -106,7 +131,7 @@ public class DefaultCookieProviderTest extends AbstractKeycloakTest {
@Test @Test
public void testSameSiteLegacyExpire() { public void testSameSiteLegacyExpire() {
filter.setHeader("Cookie", "AUTH_SESSION_ID=new;AUTH_SESSION_ID_LEGACY=legacy;KC_RESTART_LEGACY=ignore;KEYCLOAK_LOCALE=foobar"); filter.setHeader("Cookie", "AUTH_SESSION_ID=new; AUTH_SESSION_ID_LEGACY=legacy; KC_RESTART_LEGACY=ignore; KEYCLOAK_LOCALE=foobar");
Response response = testing.server().runWithResponse(session -> { Response response = testing.server().runWithResponse(session -> {
session.getProvider(CookieProvider.class).expire(CookieType.AUTH_SESSION_ID); session.getProvider(CookieProvider.class).expire(CookieType.AUTH_SESSION_ID);
@ -115,10 +140,27 @@ public class DefaultCookieProviderTest extends AbstractKeycloakTest {
Map<String, NewCookie> cookies = response.getCookies(); Map<String, NewCookie> cookies = response.getCookies();
Assert.assertEquals(2, cookies.size()); Assert.assertEquals(2, cookies.size());
assertCookie(response, "AUTH_SESSION_ID", "", "/auth/realms/master/", 0, false, false, null, true); assertCookie(response, "AUTH_SESSION_ID", "", "/auth/realms/master/", 0, false, false, null, false);
assertCookie(response, "AUTH_SESSION_ID_LEGACY", "", "/auth/realms/master/", 0, false, false, null, false);
} }
private void assertCookie(Response response, String name, String value, String path, int maxAge, boolean secure, boolean httpOnly, String sameSite, boolean hasLegacy) { @Test
public void testCustomCookie() {
Response response = testing.server().runWithResponse(session -> {
NewCookie newCookie = new NewCookie.Builder("mycookie")
.maxAge(1232)
.value("myvalue")
.path(session.getContext().getUri().getRequestUri().getRawPath())
.build();
session.getContext().getHttpResponse().setCookieIfAbsent(newCookie);
});
Map<String, NewCookie> cookies = response.getCookies();
Assert.assertEquals(1, cookies.size());
assertCookie(response, "mycookie", "myvalue", "/auth/realms/master/testing/run-on-server", 1232, false, false, null, false);
}
private void assertCookie(Response response, String name, String value, String path, int maxAge, boolean secure, boolean httpOnly, String sameSite, boolean verifyLegacy) {
Map<String, NewCookie> cookies = response.getCookies(); Map<String, NewCookie> cookies = response.getCookies();
NewCookie cookie = cookies.get(name); NewCookie cookie = cookies.get(name);
Assert.assertNotNull(cookie); Assert.assertNotNull(cookie);
@ -128,18 +170,22 @@ public class DefaultCookieProviderTest extends AbstractKeycloakTest {
Assert.assertEquals(secure || "None".equals(sameSite), cookie.isSecure()); Assert.assertEquals(secure || "None".equals(sameSite), cookie.isSecure());
Assert.assertEquals(httpOnly, cookie.isHttpOnly()); Assert.assertEquals(httpOnly, cookie.isHttpOnly());
String setHeader = (String) response.getHeaders().get("Set-Cookie").stream().filter(v -> ((String) v).startsWith(name)).findFirst().get(); String setHeader = getSetHeader(response, name);
if (sameSite == null) { if (sameSite == null) {
Assert.assertFalse(setHeader.contains("SameSite")); Assert.assertFalse(setHeader.contains("SameSite"));
} else { } else {
Assert.assertTrue(setHeader.contains("SameSite=" + sameSite)); Assert.assertTrue("Expected SameSite=" + sameSite + ", header was: " + setHeader, setHeader.contains("SameSite=" + sameSite));
} }
if (hasLegacy) { if (verifyLegacy) {
assertCookie(response, name + "_LEGACY", value, path, maxAge, secure, httpOnly, null, false); assertCookie(response, name + "_LEGACY", value, path, maxAge, secure, httpOnly, null, false);
} }
} }
private String getSetHeader(Response response, String name) {
return (String) response.getHeaders().get("Set-Cookie").stream().filter(v -> ((String) v).startsWith(name)).findFirst().get();
}
private KeycloakTestingClient createTestingClient(String serverUrl) { private KeycloakTestingClient createTestingClient(String serverUrl) {
ResteasyClientBuilder restEasyClientBuilder = KeycloakTestingClient.getRestEasyClientBuilder(serverUrl); ResteasyClientBuilder restEasyClientBuilder = KeycloakTestingClient.getRestEasyClientBuilder(serverUrl);
ResteasyClient resteasyClient = restEasyClientBuilder.build(); ResteasyClient resteasyClient = restEasyClientBuilder.build();

View file

@ -32,7 +32,7 @@ function checkAuthState(authSessionId, tabId, loginRestartUrl) {
// Attempt to parse the auth state as JSON. // Attempt to parse the auth state as JSON.
let authState; let authState;
try { try {
authState = JSON.parse(authStateRaw); authState = JSON.parse(decodeURIComponent(authStateRaw));
} catch (error) { } catch (error) {
// The auth state is not valid JSON, exit. // The auth state is not valid JSON, exit.
return; return;
@ -64,12 +64,11 @@ function getAuthState() {
} }
function getCookieByName(name) { function getCookieByName(name) {
const cookies = new Map();
for (const cookie of document.cookie.split(";")) { for (const cookie of document.cookie.split(";")) {
const [key, value] = cookie.split("=").map((value) => value.trim()); const [key, value] = cookie.split("=").map((value) => value.trim());
cookies.set(key, value); if (key === name) {
return value.startsWith('"') && value.endsWith('"') ? value.slice(1, -1) : value;
}
} }
return null;
return cookies.get(name) ?? null;
} }