Remove legacy cookies

Closes #16770

Signed-off-by: stianst <stianst@gmail.com>
Signed-off-by: Jon Koops <jonkoops@gmail.com>
Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Stian Thorgersen 2024-08-15 15:27:38 +02:00 committed by GitHub
parent 708a6898db
commit 310824cc2b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 197 additions and 195 deletions

View file

@ -119,3 +119,13 @@ It helps to find performance bottlenecks, determine the cause of application fai
The support is in preview mode, and we would be happy to obtain any feedback. The support is in preview mode, and we would be happy to obtain any feedback.
For more information, see the link:{tracingguide_link}[{tracingguide_name}] guide. For more information, see the link:{tracingguide_link}[{tracingguide_name}] guide.
= Removal of legacy cookies
Keycloak no longer sends `_LEGACY` cookies, which where introduced as a work-around to older browsers not supporting
the `SameSite` flag on cookies.
The `_LEGACY` cookies also served another purpose, which was to allow login from an insecure context. Although, this is
not recommended at all in production deployments of Keycloak, it is fairly frequent to access Keycloak over `http` outside
of `localhost`. As an alternative to the `_LEGACY` cookies Keycloak now doesn't set the `secure` flag and sets `SameSite=Lax`
instead of `SameSite=None` when it detects an insecure context is used.

View file

@ -4,6 +4,12 @@ import jakarta.annotation.Nullable;
public final class CookieType { public final class CookieType {
public static final CookieType[] OLD_UNUSED_COOKIES = new CookieType[] {
CookieType.create("AUTH_SESSION_ID_LEGACY").build(),
CookieType.create("KEYCLOAK_IDENTITY_LEGACY").build(),
CookieType.create("KEYCLOAK_SESSION_LEGACY").build()
};
public static final CookieType AUTH_DETACHED = CookieType.create("KC_STATE_CHECKER") public static final CookieType AUTH_DETACHED = CookieType.create("KC_STATE_CHECKER")
.scope(CookieScope.INTERNAL) .scope(CookieScope.INTERNAL)
.build(); .build();
@ -16,12 +22,10 @@ public final class CookieType {
public static final CookieType AUTH_SESSION_ID = CookieType.create("AUTH_SESSION_ID") public static final CookieType AUTH_SESSION_ID = CookieType.create("AUTH_SESSION_ID")
.scope(CookieScope.FEDERATION) .scope(CookieScope.FEDERATION)
.defaultMaxAge(CookieMaxAge.SESSION) .defaultMaxAge(CookieMaxAge.SESSION)
.supportSameSiteLegacy()
.build(); .build();
public static final CookieType IDENTITY = CookieType.create("KEYCLOAK_IDENTITY") public static final CookieType IDENTITY = CookieType.create("KEYCLOAK_IDENTITY")
.scope(CookieScope.FEDERATION) .scope(CookieScope.FEDERATION)
.supportSameSiteLegacy()
.build(); .build();
public static final CookieType LOCALE = CookieType.create("KEYCLOAK_LOCALE") public static final CookieType LOCALE = CookieType.create("KEYCLOAK_LOCALE")
@ -36,7 +40,6 @@ public final class CookieType {
public static final CookieType SESSION = CookieType.create("KEYCLOAK_SESSION") public static final CookieType SESSION = CookieType.create("KEYCLOAK_SESSION")
.scope(CookieScope.FEDERATION_JS) .scope(CookieScope.FEDERATION_JS)
.supportSameSiteLegacy()
.build(); .build();
public static final CookieType WELCOME_CSRF = CookieType.create("WELCOME_STATE_CHECKER") public static final CookieType WELCOME_CSRF = CookieType.create("WELCOME_STATE_CHECKER")
@ -45,15 +48,13 @@ public final class CookieType {
.build(); .build();
private final String name; private final String name;
private final String sameSiteLegacyName;
private final CookiePath path; private final CookiePath path;
private final CookieScope scope; private final CookieScope scope;
private final Integer defaultMaxAge; private final Integer defaultMaxAge;
private CookieType(String name, boolean supportsSameSiteLegacy, CookiePath path, CookieScope scope, @Nullable Integer defaultMaxAge) { private CookieType(String name, CookiePath path, CookieScope scope, @Nullable Integer defaultMaxAge) {
this.name = name; this.name = name;
this.sameSiteLegacyName = supportsSameSiteLegacy ? name + "_LEGACY" : null;
this.path = path; this.path = path;
this.scope = scope; this.scope = scope;
this.defaultMaxAge = defaultMaxAge; this.defaultMaxAge = defaultMaxAge;
@ -67,16 +68,6 @@ public final class CookieType {
return name; return name;
} }
@Deprecated
public boolean supportsSameSiteLegacy() {
return sameSiteLegacyName != null;
}
@Deprecated
public String getSameSiteLegacyName() {
return sameSiteLegacyName;
}
public CookiePath getPath() { public CookiePath getPath() {
return path; return path;
} }
@ -92,7 +83,6 @@ public final class CookieType {
private static class CookieTypeBuilder { private static class CookieTypeBuilder {
private String name; private String name;
private boolean supportSameSiteLegacy = false;
private CookiePath path = CookiePath.REALM; private CookiePath path = CookiePath.REALM;
private CookieScope scope = CookieScope.INTERNAL; private CookieScope scope = CookieScope.INTERNAL;
private Integer defaultMaxAge; private Integer defaultMaxAge;
@ -111,18 +101,13 @@ public final class CookieType {
return this; return this;
} }
CookieTypeBuilder supportSameSiteLegacy() {
this.supportSameSiteLegacy = true;
return this;
}
CookieTypeBuilder defaultMaxAge(int defaultMaxAge) { CookieTypeBuilder defaultMaxAge(int defaultMaxAge) {
this.defaultMaxAge = defaultMaxAge; this.defaultMaxAge = defaultMaxAge;
return this; return this;
} }
CookieType build() { CookieType build() {
return new CookieType(name, supportSameSiteLegacy, path, scope, defaultMaxAge); return new CookieType(name, path, scope, defaultMaxAge);
} }
} }

View file

@ -1,39 +0,0 @@
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

@ -12,25 +12,27 @@ public class DefaultCookieProvider implements CookieProvider {
private final KeycloakContext context; private final KeycloakContext context;
private CookiePathResolver pathResolver; private final CookiePathResolver pathResolver;
private CookieSecureResolver secureResolver; private final boolean secure;
private final Map<String, Cookie> cookies; private final Map<String, Cookie> cookies;
private final boolean sameSiteLegacyEnabled; public DefaultCookieProvider(KeycloakContext context) {
public DefaultCookieProvider(KeycloakContext context, boolean sameSiteLegacyEnabled) {
this.context = context; this.context = context;
this.cookies = context.getRequestHeaders().getCookies(); this.cookies = context.getRequestHeaders().getCookies();
this.pathResolver = new CookiePathResolver(context); this.pathResolver = new CookiePathResolver(context);
this.secureResolver = new CookieSecureResolver(context, sameSiteLegacyEnabled); this.secure = SecureContextResolver.isSecureContext(context.getUri().getRequestUri());
this.sameSiteLegacyEnabled = sameSiteLegacyEnabled;
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
String cookieNames = String.join(", ", this.cookies.keySet()); logger.tracef("Received cookies: %s, path: %s", String.join(", ", this.cookies.keySet()), context.getUri().getRequestUri().getRawPath());
logger.tracef("Path: %s, cookies: %s", context.getUri().getRequestUri().getRawPath(), cookieNames);
} }
if (!secure) {
logger.warnf("Non-secure context detected; cookies are not secured, and will not be available in cross-origin POST requests");
}
expireOldUnusedCookies();
} }
@Override @Override
@ -46,7 +48,10 @@ public class DefaultCookieProvider implements CookieProvider {
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();
NewCookie.SameSite sameSite = cookieType.getScope().getSameSite(); NewCookie.SameSite sameSite = cookieType.getScope().getSameSite();
boolean secure = secureResolver.resolveSecure(sameSite); if (NewCookie.SameSite.NONE.equals(sameSite) && !secure) {
sameSite = NewCookie.SameSite.LAX;
}
String path = pathResolver.resolvePath(cookieType); String path = pathResolver.resolvePath(cookieType);
boolean httpOnly = cookieType.getScope().isHttpOnly(); boolean httpOnly = cookieType.getScope().isHttpOnly();
@ -63,63 +68,17 @@ public class DefaultCookieProvider implements CookieProvider {
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);
setSameSiteLegacy(cookieType, value, maxAge);
}
private void setSameSiteLegacy(CookieType cookieType, String value, int maxAge) {
if (sameSiteLegacyEnabled && cookieType.supportsSameSiteLegacy()) {
String legacyName = cookieType.getSameSiteLegacyName();
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) {
cookie = getSameSiteLegacyCookie(cookieType);
}
return cookie != null ? cookie.getValue() : null; return cookie != null ? cookie.getValue() : null;
} }
private Cookie getSameSiteLegacyCookie(CookieType cookieType) {
if (cookieType.supportsSameSiteLegacy()) {
return cookies.get(cookieType.getSameSiteLegacyName());
} else {
return null;
}
}
@Override @Override
public void expire(CookieType cookieType) { public void expire(CookieType cookieType) {
expire(cookieType.getName(), cookieType); String cookieName = cookieType.getName();
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); Cookie cookie = cookies.get(cookieName);
if (cookie != null) { if (cookie != null) {
String path = pathResolver.resolvePath(cookieType); String path = pathResolver.resolvePath(cookieType);
@ -135,6 +94,12 @@ public class DefaultCookieProvider implements CookieProvider {
} }
} }
private void expireOldUnusedCookies() {
for (CookieType cookieType : CookieType.OLD_UNUSED_COOKIES) {
expire(cookieType);
}
}
@Override @Override
public void close() { public void close() {
} }

View file

@ -3,24 +3,16 @@ 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 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(), sameSiteLegacyEnabled); return new DefaultCookieProvider(session.getContext());
} }
@Override @Override
public void init(Config.Scope config) { public void init(Config.Scope config) {
sameSiteLegacyEnabled = config.getBoolean(SAME_SITE_LEGACY_KEY, true);
} }
@Override @Override
@ -36,15 +28,4 @@ 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

@ -0,0 +1,46 @@
package org.keycloak.cookie;
import java.net.URI;
class SecureContextResolver {
/**
* Determines if a URI is potentially trustworthy, meaning a user agent can generally trust it to deliver data securely.
*
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts">MDN Web Docs Secure Contexts</a>
* @see <a href="https://w3c.github.io/webappsec-secure-contexts/#algorithms">W3C Secure Contexts specification Is origin potentially trustworthy?</a>
* @param uri The URI to check.
* @return Whether the URI can be considered potentially trustworthy.
*/
static boolean isSecureContext(URI uri) {
if (uri.getScheme().equals("https")) {
return true;
}
String host = uri.getHost();
if (host == null) {
return false;
}
// The host matches a CIDR notation of ::1/128
if (host.equals("[::1]") || host.equals("[0000:0000:0000:0000:0000:0000:0000:0001]")) {
return true;
}
// The host matches a CIDR notation of 127.0.0.0/8
if (host.matches("127.\\d{1,3}.\\d{1,3}.\\d{1,3}")) {
return true;
}
if (host.equals("localhost") || host.equals("localhost.")) {
return true;
}
if (host.endsWith(".localhost") || host.endsWith(".localhost.")) {
return true;
}
return false;
}
}

View file

@ -108,12 +108,7 @@
function getSessionCookie() { function getSessionCookie() {
const cookie = getCookieByName("KEYCLOAK_SESSION"); const cookie = getCookieByName("KEYCLOAK_SESSION");
return cookie;
if (cookie !== null) {
return cookie;
}
return getCookieByName("KEYCLOAK_SESSION_LEGACY");
} }
function getCookieByName(name) { function getCookieByName(name) {

View file

@ -0,0 +1,58 @@
package org.keycloak.cookie;
import org.junit.Assert;
import org.junit.Test;
import java.net.URI;
import java.net.URISyntaxException;
public class SecureContextResolverTest {
@Test
public void testHttps() {
assertSecureContext("https://127.0.0.1", true);
assertSecureContext("https://something", true);
}
@Test
public void testIp4() {
assertSecureContext("http://127.0.0.1", true);
assertSecureContext("http://127.0.0.128", true);
assertSecureContext("http://127.0.0.255", true);
assertSecureContext("http://127.0.0.256", false);
assertSecureContext("http://127.0.1.1", true);
assertSecureContext("http://127.254.232.123", true);
assertSecureContext("http://127.256.232.123", false);
assertSecureContext("http://10.0.0.10", false);
assertSecureContext("http://127.0.0.1.nip.io", false);
}
@Test
public void testIp6() {
assertSecureContext("http://[::1]", true);
assertSecureContext("http://[0000:0000:0000:0000:0000:0000:0000:0001]", true);
assertSecureContext("http://[::2]", false);
assertSecureContext("http://[2001:0000:130F:0000:0000:09C0:876A:130B]", false);
assertSecureContext("http://::1", false);
}
@Test
public void testLocalhost() {
assertSecureContext("http://localhost", true);
assertSecureContext("http://localhost.", true);
assertSecureContext("http://localhostn", false);
assertSecureContext("http://test.localhost", true);
assertSecureContext("http://test.localhost.", true);
assertSecureContext("http://test.localhostn", false);
assertSecureContext("http://test.localhost.not", false);
}
void assertSecureContext(String url, boolean expectedSecureContext) {
try {
Assert.assertEquals(expectedSecureContext, SecureContextResolver.isSecureContext(new URI(url)));
} catch (URISyntaxException e) {
Assert.fail(e.getMessage());
}
}
}

View file

@ -175,7 +175,7 @@ public class SimpleUndertowLoadBalancer {
private HttpHandler createHandler() throws Exception { private HttpHandler createHandler() throws Exception {
// TODO: configurable options if needed // TODO: configurable options if needed
String[] sessionIds = {CookieType.AUTH_SESSION_ID.getName(), CookieType.AUTH_SESSION_ID.getSameSiteLegacyName()}; String[] sessionIds = {CookieType.AUTH_SESSION_ID.getName()};
int connectionsPerThread = 20; int connectionsPerThread = 20;
int problemServerRetry = 5; // In case of unavailable node, we will try to ping him every 5 seconds to check if it's back int problemServerRetry = 5; // In case of unavailable node, we will try to ping him every 5 seconds to check if it's back
int maxTime = 3600000; // 1 hour for proxy request timeout, so we can debug the backend keycloak servers int maxTime = 3600000; // 1 hour for proxy request timeout, so we can debug the backend keycloak servers
@ -233,11 +233,11 @@ public class SimpleUndertowLoadBalancer {
@Override @Override
protected Iterator<CharSequence> parseRoutes(HttpServerExchange exchange) { protected Iterator<CharSequence> parseRoutes(HttpServerExchange exchange) {
Iterator<CharSequence> stickyHostsIt = super.parseRoutes(exchange); Iterator<CharSequence> stickyHostsIt = super.parseRoutes(exchange);
if (stickyHostsIt == null) { if (stickyHostsIt == null) {
return null; return null;
} }
List<CharSequence> stickyHosts = new LinkedList<>(); List<CharSequence> stickyHosts = new LinkedList<>();
stickyHostsIt.forEachRemaining(stickyHosts::add); stickyHostsIt.forEachRemaining(stickyHosts::add);
CharSequence stickyHostName = stickyHosts.isEmpty() ? null : stickyHosts.iterator().next(); CharSequence stickyHostName = stickyHosts.isEmpty() ? null : stickyHosts.iterator().next();

View file

@ -1819,9 +1819,7 @@ public class SAMLServletAdapterTest extends AbstractSAMLServletAdapterTest {
Cookie identityCookie = driver.manage().getCookieNamed(CookieType.IDENTITY.getName()); Cookie identityCookie = driver.manage().getCookieNamed(CookieType.IDENTITY.getName());
Assert.assertNotNull(identityCookie); Assert.assertNotNull(identityCookie);
driver.manage().deleteCookieNamed(CookieType.AUTH_SESSION_ID.getName()); driver.manage().deleteCookieNamed(CookieType.AUTH_SESSION_ID.getName());
driver.manage().deleteCookieNamed(CookieType.AUTH_SESSION_ID.getSameSiteLegacyName());
driver.manage().addCookie(new Cookie(CookieType.AUTH_SESSION_ID.getName(), "invalid-value", identityCookie.getPath())); driver.manage().addCookie(new Cookie(CookieType.AUTH_SESSION_ID.getName(), "invalid-value", identityCookie.getPath()));
driver.manage().addCookie(new Cookie(CookieType.AUTH_SESSION_ID.getSameSiteLegacyName(), "invalid-value", identityCookie.getPath()));
// go back to the app page, re-login should work with the invalid cookie // go back to the app page, re-login should work with the invalid cookie
testRealmSAMLPostLoginPage.navigateTo(); testRealmSAMLPostLoginPage.navigateTo();

View file

@ -97,10 +97,8 @@ public class KcOidcBrokerLogoutTest extends AbstractKcOidcBrokerLogoutTest {
String idToken = response.getIdToken(); String idToken = response.getIdToken();
// simulate browser restart by deleting an identity cookie // simulate browser restart by deleting an identity cookie
log.debugf("Deleting %s and %s cookies", CookieType.IDENTITY.getName(), log.debugf("Deleting %s cookie", CookieType.IDENTITY.getName());
CookieType.IDENTITY.getSameSiteLegacyName());
driver.manage().deleteCookieNamed(CookieType.IDENTITY.getName()); driver.manage().deleteCookieNamed(CookieType.IDENTITY.getName());
driver.manage().deleteCookieNamed(CookieType.IDENTITY.getSameSiteLegacyName());
AccountHelper.logout(adminClient.realm(bc.consumerRealmName()), bc.getUserLogin()); AccountHelper.logout(adminClient.realm(bc.consumerRealmName()), bc.getUserLogin());
AccountHelper.logout(adminClient.realm(bc.providerRealmName()), bc.getUserLogin()); AccountHelper.logout(adminClient.realm(bc.providerRealmName()), bc.getUserLogin());

View file

@ -113,9 +113,6 @@ public class AuthenticationSessionFailoverClusterTest extends AbstractFailoverCl
public static String getAuthSessionCookieValue(WebDriver driver) { public static String getAuthSessionCookieValue(WebDriver driver) {
Cookie authSessionCookie = driver.manage().getCookieNamed(CookieType.AUTH_SESSION_ID.getName()); Cookie authSessionCookie = driver.manage().getCookieNamed(CookieType.AUTH_SESSION_ID.getName());
if (authSessionCookie == null) {
authSessionCookie = driver.manage().getCookieNamed(CookieType.AUTH_SESSION_ID.getSameSiteLegacyName());
}
Assert.assertNotNull(authSessionCookie); Assert.assertNotNull(authSessionCookie);
return authSessionCookie.getValue(); return authSessionCookie.getValue();
} }

View file

@ -40,6 +40,7 @@ public class DefaultCookieProviderTest extends AbstractKeycloakTest {
@Test @Test
public void testCookieDefaults() { public void testCookieDefaults() {
Response response = testing.server("master").runWithResponse(session -> { Response response = testing.server("master").runWithResponse(session -> {
CookieProvider cookies = session.getProvider(CookieProvider.class); CookieProvider cookies = session.getProvider(CookieProvider.class);
cookies.set(CookieType.AUTH_SESSION_ID, "my-auth-session-id"); cookies.set(CookieType.AUTH_SESSION_ID, "my-auth-session-id");
cookies.set(CookieType.AUTH_RESTART, "my-auth-restart"); cookies.set(CookieType.AUTH_RESTART, "my-auth-restart");
@ -50,14 +51,40 @@ public class DefaultCookieProviderTest extends AbstractKeycloakTest {
cookies.set(CookieType.SESSION, "my-session", 444); cookies.set(CookieType.SESSION, "my-session", 444);
cookies.set(CookieType.WELCOME_CSRF, "my-welcome-csrf"); cookies.set(CookieType.WELCOME_CSRF, "my-welcome-csrf");
}); });
Assert.assertEquals(11, response.getCookies().size()); Assert.assertEquals(8, response.getCookies().size());
assertCookie(response, "AUTH_SESSION_ID", "my-auth-session-id", "/auth/realms/master/", -1, false, true, "None", true); assertCookie(response, "AUTH_SESSION_ID", "my-auth-session-id", "/auth/realms/master/", -1, true, true, "None", true);
assertCookie(response, "KC_RESTART", "my-auth-restart", "/auth/realms/master/", -1, false, true, "None", false); assertCookie(response, "KC_RESTART", "my-auth-restart", "/auth/realms/master/", -1, true, true, "None", false);
assertCookie(response, "KC_STATE_CHECKER", "my-auth-detached", "/auth/realms/master/", 222, true, true, "Strict", false);
assertCookie(response, "KEYCLOAK_IDENTITY", "my-identity", "/auth/realms/master/", 333, true, true, "None", true);
assertCookie(response, "KEYCLOAK_LOCALE", "my-locale", "/auth/realms/master/", -1, true, true, "None", false);
assertCookie(response, "KEYCLOAK_REMEMBER_ME", "my-username", "/auth/realms/master/", 31536000, true, true, "None", false);
assertCookie(response, "KEYCLOAK_SESSION", "my-session", "/auth/realms/master/", 444, true, false, "None", true);
assertCookie(response, "WELCOME_STATE_CHECKER", "my-welcome-csrf", "/auth/realms/master/testing/run-on-server", 300, true, true, "Strict", false);
}
@Test
public void testCookieDefaultsWithInsecureContext() {
KeycloakTestingClient testingInsecure = KeycloakTestingClient.getInstance("http://127.0.0.1.nip.io:8180/auth");
Response response = testingInsecure.server("master").runWithResponse(session -> {
CookieProvider cookies = session.getProvider(CookieProvider.class);
cookies.set(CookieType.AUTH_SESSION_ID, "my-auth-session-id");
cookies.set(CookieType.AUTH_RESTART, "my-auth-restart");
cookies.set(CookieType.AUTH_DETACHED, "my-auth-detached", 222);
cookies.set(CookieType.IDENTITY, "my-identity", 333);
cookies.set(CookieType.LOCALE, "my-locale");
cookies.set(CookieType.LOGIN_HINT, "my-username");
cookies.set(CookieType.SESSION, "my-session", 444);
cookies.set(CookieType.WELCOME_CSRF, "my-welcome-csrf");
});
Assert.assertEquals(8, response.getCookies().size());
assertCookie(response, "AUTH_SESSION_ID", "my-auth-session-id", "/auth/realms/master/", -1, false, true, "Lax", true);
assertCookie(response, "KC_RESTART", "my-auth-restart", "/auth/realms/master/", -1, false, true, "Lax", false);
assertCookie(response, "KC_STATE_CHECKER", "my-auth-detached", "/auth/realms/master/", 222, false, true, "Strict", false); assertCookie(response, "KC_STATE_CHECKER", "my-auth-detached", "/auth/realms/master/", 222, false, true, "Strict", false);
assertCookie(response, "KEYCLOAK_IDENTITY", "my-identity", "/auth/realms/master/", 333, false, true, "None", true); assertCookie(response, "KEYCLOAK_IDENTITY", "my-identity", "/auth/realms/master/", 333, false, true, "Lax", true);
assertCookie(response, "KEYCLOAK_LOCALE", "my-locale", "/auth/realms/master/", -1, false, true, "None", false); assertCookie(response, "KEYCLOAK_LOCALE", "my-locale", "/auth/realms/master/", -1, false, true, "Lax", false);
assertCookie(response, "KEYCLOAK_REMEMBER_ME", "my-username", "/auth/realms/master/", 31536000, false, true, "None", false); assertCookie(response, "KEYCLOAK_REMEMBER_ME", "my-username", "/auth/realms/master/", 31536000, false, true, "Lax", false);
assertCookie(response, "KEYCLOAK_SESSION", "my-session", "/auth/realms/master/", 444, false, false, "None", true); assertCookie(response, "KEYCLOAK_SESSION", "my-session", "/auth/realms/master/", 444, false, false, "Lax", true);
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);
} }
@ -112,36 +139,22 @@ public class DefaultCookieProviderTest extends AbstractKeycloakTest {
} }
@Test @Test
public void testSameSiteLegacyGet() { public void testExpireOldUnused() {
filter.setHeader("Cookie", "AUTH_SESSION_ID=new;AUTH_SESSION_ID_LEGACY=legacy;KC_RESTART_LEGACY=ignore"); filter.setHeader("Cookie", "AUTH_SESSION_ID_LEGACY=legacy; KEYCLOAK_IDENTITY_LEGACY=legacy; KEYCLOAK_SESSION_LEGACY=ignore");
testing.server().run(session -> {
Assert.assertEquals("new", session.getProvider(CookieProvider.class).get(CookieType.AUTH_SESSION_ID));
Assert.assertEquals(null, session.getProvider(CookieProvider.class).get(CookieType.AUTH_RESTART));
});
filter.setHeader("Cookie", "AUTH_SESSION_ID_LEGACY=legacy");
testing.server().run(session -> {
Assert.assertEquals("legacy", session.getProvider(CookieProvider.class).get(CookieType.AUTH_SESSION_ID));
});
}
@Test
public void testSameSiteLegacyExpire() {
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); Assert.assertNull(session.getProvider(CookieProvider.class).get(CookieType.AUTH_SESSION_ID));
session.getProvider(CookieProvider.class).expire(CookieType.AUTH_RESTART); Assert.assertNull(session.getProvider(CookieProvider.class).get(CookieType.IDENTITY));
Assert.assertNull(session.getProvider(CookieProvider.class).get(CookieType.SESSION));
}); });
Map<String, NewCookie> cookies = response.getCookies(); Map<String, NewCookie> cookies = response.getCookies();
Assert.assertEquals(2, cookies.size()); Assert.assertEquals(3, cookies.size());
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); assertCookie(response, "AUTH_SESSION_ID_LEGACY", "", "/auth/realms/master/", 0, false, false, null, false);
assertCookie(response, "KEYCLOAK_IDENTITY_LEGACY", "", "/auth/realms/master/", 0, false, false, null, false);
assertCookie(response, "KEYCLOAK_SESSION_LEGACY", "", "/auth/realms/master/", 0, false, false, null, false);
} }
@Test @Test
public void testCustomCookie() { public void testCustomCookie() {
Response response = testing.server().runWithResponse(session -> { Response response = testing.server().runWithResponse(session -> {
@ -158,14 +171,14 @@ public class DefaultCookieProviderTest extends AbstractKeycloakTest {
assertCookie(response, "mycookie", "myvalue", "/auth/realms/master/testing/run-on-server", 1232, false, false, null, false); 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) { private void assertCookie(Response response, String name, String value, String path, int maxAge, boolean secure, boolean httpOnly, String sameSite, boolean verifyLegacyNotSent) {
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);
Assert.assertEquals(value, cookie.getValue()); Assert.assertEquals(value, cookie.getValue());
Assert.assertEquals(path, cookie.getPath()); Assert.assertEquals(path, cookie.getPath());
Assert.assertEquals(maxAge, cookie.getMaxAge()); Assert.assertEquals(maxAge, cookie.getMaxAge());
Assert.assertEquals(secure || "None".equals(sameSite), cookie.isSecure()); Assert.assertEquals(secure, cookie.isSecure());
Assert.assertEquals(httpOnly, cookie.isHttpOnly()); Assert.assertEquals(httpOnly, cookie.isHttpOnly());
String setHeader = getSetHeader(response, name); String setHeader = getSetHeader(response, name);
@ -175,8 +188,8 @@ public class DefaultCookieProviderTest extends AbstractKeycloakTest {
Assert.assertTrue("Expected SameSite=" + sameSite + ", header was: " + setHeader, setHeader.contains("SameSite=" + sameSite)); Assert.assertTrue("Expected SameSite=" + sameSite + ", header was: " + setHeader, setHeader.contains("SameSite=" + sameSite));
} }
if (verifyLegacy) { if (verifyLegacyNotSent) {
assertCookie(response, name + "_LEGACY", value, path, maxAge, secure, httpOnly, null, false); Assert.assertNull(response.getCookies().get(name + "_LEGACY"));
} }
} }

View file

@ -266,16 +266,11 @@ public class UserStorageTest extends AbstractAuthTest {
driver.navigate().to(oauth.AUTH_SERVER_ROOT + "/realms/" + testRealmResource().toRepresentation().getRealm() + "/login-actions/authenticate/" ); driver.navigate().to(oauth.AUTH_SERVER_ROOT + "/realms/" + testRealmResource().toRepresentation().getRealm() + "/login-actions/authenticate/" );
Cookie sameSiteSessionCookie = driver.manage().getCookieNamed(CookieType.SESSION.getName()); Cookie sameSiteSessionCookie = driver.manage().getCookieNamed(CookieType.SESSION.getName());
Cookie legacySessionCookie = driver.manage().getCookieNamed(CookieType.SESSION.getSameSiteLegacyName());
String cookieValue = sameSiteSessionCookie.getValue(); String cookieValue = sameSiteSessionCookie.getValue();
assertThat(cookieValue.contains("spécial"), is(false)); assertThat(cookieValue.contains("spécial"), is(false));
assertThat(cookieValue.contains("sp%C3%A9cial"), is(true)); assertThat(cookieValue.contains("sp%C3%A9cial"), is(true));
String legacyCookieValue = legacySessionCookie.getValue();
assertThat(legacyCookieValue.contains("spécial"), is(false));
assertThat(legacyCookieValue.contains("sp%C3%A9cial"), is(true));
AccountHelper.logout(testRealmResource(), "spécial"); AccountHelper.logout(testRealmResource(), "spécial");
} }