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:
parent
708a6898db
commit
310824cc2b
14 changed files with 197 additions and 195 deletions
|
@ -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.
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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() {
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue