forked from keycloak/keycloak
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Closes keycloak#26500 Signed-off-by: stianst <[email protected]>
- Loading branch information
Showing
24 changed files
with
376 additions
and
98 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 9 additions & 0 deletions
9
server-spi-private/src/main/java/org/keycloak/cookie/CookieMaxAge.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package org.keycloak.cookie; | ||
|
||
public interface CookieMaxAge { | ||
|
||
int EXPIRED = 0; | ||
|
||
int SESSION = -1; | ||
|
||
} |
6 changes: 6 additions & 0 deletions
6
server-spi-private/src/main/java/org/keycloak/cookie/CookiePath.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package org.keycloak.cookie; | ||
|
||
enum CookiePath { | ||
REALM, | ||
REQUEST | ||
} |
15 changes: 15 additions & 0 deletions
15
server-spi-private/src/main/java/org/keycloak/cookie/CookieProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package org.keycloak.cookie; | ||
|
||
import org.keycloak.provider.Provider; | ||
|
||
public interface CookieProvider extends Provider { | ||
|
||
void set(CookieType cookieType, String value); | ||
|
||
void set(CookieType cookieType, String value, int maxAge); | ||
|
||
String get(CookieType cookieType); | ||
|
||
void expire(CookieType cookieType); | ||
|
||
} |
6 changes: 6 additions & 0 deletions
6
server-spi-private/src/main/java/org/keycloak/cookie/CookieProviderFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package org.keycloak.cookie; | ||
|
||
import org.keycloak.provider.ProviderFactory; | ||
|
||
public interface CookieProviderFactory extends ProviderFactory<CookieProvider> { | ||
} |
39 changes: 39 additions & 0 deletions
39
server-spi-private/src/main/java/org/keycloak/cookie/CookieScope.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package org.keycloak.cookie; | ||
|
||
import org.keycloak.common.util.ServerCookie; | ||
|
||
enum CookieScope { | ||
// Internal cookies are only available for direct requests to Keycloak | ||
INTERNAL(ServerCookie.SameSiteAttributeValue.STRICT, true), | ||
|
||
// Federation cookies are available after redirect from applications, and are also available in an iframe context | ||
// unless the browser blocks third-party cookies | ||
FEDERATION(ServerCookie.SameSiteAttributeValue.NONE, true), | ||
|
||
// Federation cookies that are also available from JavaScript | ||
FEDERATION_JS(ServerCookie.SameSiteAttributeValue.NONE, false), | ||
|
||
// Legacy cookies do not set the SameSite attribute and will default to SameSite=Lax in modern browsers | ||
@Deprecated | ||
LEGACY(null, true), | ||
|
||
// Legacy cookies that are also available from JavaScript | ||
@Deprecated | ||
LEGACY_JS(null, false); | ||
|
||
private final ServerCookie.SameSiteAttributeValue sameSite; | ||
private final boolean httpOnly; | ||
|
||
CookieScope(ServerCookie.SameSiteAttributeValue sameSite, boolean httpOnly) { | ||
this.sameSite = sameSite; | ||
this.httpOnly = httpOnly; | ||
} | ||
|
||
public ServerCookie.SameSiteAttributeValue getSameSite() { | ||
return sameSite; | ||
} | ||
|
||
public boolean isHttpOnly() { | ||
return httpOnly; | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
server-spi-private/src/main/java/org/keycloak/cookie/CookieSpi.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package org.keycloak.cookie; | ||
|
||
import org.keycloak.provider.Provider; | ||
import org.keycloak.provider.ProviderFactory; | ||
import org.keycloak.provider.Spi; | ||
|
||
public class CookieSpi implements Spi { | ||
@Override | ||
public boolean isInternal() { | ||
return true; | ||
} | ||
|
||
@Override | ||
public String getName() { | ||
return "cookie"; | ||
} | ||
|
||
@Override | ||
public Class<? extends Provider> getProviderClass() { | ||
return CookieProvider.class; | ||
} | ||
|
||
@Override | ||
public Class<? extends ProviderFactory> getProviderFactoryClass() { | ||
return CookieProviderFactory.class; | ||
} | ||
} |
39 changes: 39 additions & 0 deletions
39
server-spi-private/src/main/java/org/keycloak/cookie/CookieType.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package org.keycloak.cookie; | ||
|
||
public enum CookieType { | ||
|
||
KEYCLOAK_LOCALE(CookiePath.REALM, CookieScope.INTERNAL, CookieMaxAge.SESSION), | ||
WELCOME_STATE_CHECKER(CookiePath.REQUEST, CookieScope.INTERNAL, 300), | ||
KC_AUTH_STATE(CookiePath.REALM, CookieScope.LEGACY_JS), // TODO Change CookieScope | ||
KC_RESTART(CookiePath.REALM, CookieScope.LEGACY, CookieMaxAge.SESSION); // TODO Change CookieScope | ||
|
||
private final CookiePath path; | ||
private final CookieScope scope; | ||
|
||
private final Integer defaultMaxAge; | ||
|
||
CookieType(CookiePath path, CookieScope scope) { | ||
this.path = path; | ||
this.scope = scope; | ||
this.defaultMaxAge = null; | ||
} | ||
|
||
CookieType(CookiePath path, CookieScope scope, int defaultMaxAge) { | ||
this.path = path; | ||
this.scope = scope; | ||
this.defaultMaxAge = defaultMaxAge; | ||
} | ||
|
||
public CookiePath getPath() { | ||
return path; | ||
} | ||
|
||
public CookieScope getScope() { | ||
return scope; | ||
} | ||
|
||
public Integer getDefaultMaxAge() { | ||
return defaultMaxAge; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
126 changes: 126 additions & 0 deletions
126
services/src/main/java/org/keycloak/cookie/DefaultCookieProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
package org.keycloak.cookie; | ||
|
||
import jakarta.ws.rs.core.Cookie; | ||
import org.keycloak.common.util.ServerCookie; | ||
import org.keycloak.http.HttpCookie; | ||
import org.keycloak.models.KeycloakSession; | ||
import org.keycloak.models.RealmModel; | ||
import org.keycloak.services.resources.RealmsResource; | ||
import org.keycloak.urls.UrlType; | ||
|
||
import java.net.URI; | ||
|
||
public class DefaultCookieProvider implements CookieProvider { | ||
|
||
private static final String LEGACY_SUFFIX = "_LEGACY"; | ||
|
||
private KeycloakSession session; | ||
|
||
private boolean legacyCookiesEnabled; | ||
|
||
public DefaultCookieProvider(KeycloakSession session, boolean legacyCookiesEnabled) { | ||
this.session = session; | ||
this.legacyCookiesEnabled = legacyCookiesEnabled; | ||
} | ||
|
||
@Override | ||
public void set(CookieType cookieType, String value) { | ||
if (cookieType.getDefaultMaxAge() == null) { | ||
throw new IllegalArgumentException(cookieType + " has no default max-age"); | ||
} | ||
|
||
set(cookieType, value, cookieType.getDefaultMaxAge()); | ||
} | ||
|
||
@Override | ||
public void set(CookieType cookieType, String value, int maxAge) { | ||
String name = cookieType.name(); | ||
ServerCookie.SameSiteAttributeValue sameSite = cookieType.getScope().getSameSite(); | ||
boolean secure = resolveSecure(sameSite); | ||
String path = resolvePath(cookieType); | ||
boolean httpOnly = cookieType.getScope().isHttpOnly(); | ||
|
||
HttpCookie newCookie = new HttpCookie(1, name, value, path, null, null, maxAge, secure, httpOnly, sameSite); | ||
session.getContext().getHttpResponse().setCookieIfAbsent(newCookie); | ||
|
||
if (legacyCookiesEnabled) { | ||
if (ServerCookie.SameSiteAttributeValue.NONE.equals(sameSite)) { | ||
String legacyName = name + LEGACY_SUFFIX; | ||
HttpCookie legacyCookie = new HttpCookie(1, legacyName, value, path, null, null, maxAge, secure, httpOnly, null); | ||
session.getContext().getHttpResponse().setCookieIfAbsent(legacyCookie); | ||
} | ||
} else { | ||
expireLegacy(cookieType); | ||
} | ||
} | ||
|
||
@Override | ||
public String get(CookieType cookieType) { | ||
Cookie cookie = session.getContext().getRequestHeaders().getCookies().get(cookieType.name()); | ||
return cookie != null ? cookie.getValue() : null; | ||
} | ||
|
||
@Override | ||
public void expire(CookieType cookieType) { | ||
Cookie cookie = session.getContext().getRequestHeaders().getCookies().get(cookieType.name()); | ||
expire(cookie, cookieType); | ||
|
||
expireLegacy(cookieType); | ||
} | ||
|
||
private void expireLegacy(CookieType cookieType) { | ||
String legacyName = cookieType.name() + LEGACY_SUFFIX; | ||
Cookie legacyCookie = session.getContext().getRequestHeaders().getCookies().get(legacyName); | ||
expire(legacyCookie, cookieType); | ||
} | ||
|
||
private void expire(Cookie cookie, CookieType cookieType) { | ||
if (cookie != null) { | ||
String path = resolvePath(cookieType); | ||
HttpCookie newCookie = new HttpCookie(1, cookie.getName(), "", path, null, null, 0, false, false, null); | ||
session.getContext().getHttpResponse().setCookieIfAbsent(newCookie); | ||
} | ||
} | ||
|
||
@Override | ||
public void close() { | ||
} | ||
|
||
private String resolvePath(CookieType cookieType) { | ||
switch (cookieType.getPath()) { | ||
case REALM: | ||
return RealmsResource.realmBaseUrl(session.getContext().getUri()).path("/").build(session.getContext().getRealm().getName()).getRawPath(); | ||
case REQUEST: | ||
return session.getContext().getUri().getRequestUri().getRawPath(); | ||
default: | ||
throw new IllegalArgumentException("Unsupported enum value " + cookieType.getPath().name()); | ||
} | ||
} | ||
|
||
private boolean resolveSecure(ServerCookie.SameSiteAttributeValue sameSite) { | ||
URI requestUri = session.getContext().getUri().getRequestUri(); | ||
|
||
// SameSite=none requires secure context | ||
if (ServerCookie.SameSiteAttributeValue.NONE.equals(sameSite)) { | ||
return true; | ||
} | ||
|
||
RealmModel realm = session.getContext().getRealm(); | ||
if (realm != null && realm.getSslRequired().isRequired(requestUri.getHost())) { | ||
return true; | ||
} | ||
|
||
if ("https".equals(requestUri.getScheme())) { | ||
return true; | ||
} | ||
|
||
// Browsers consider 127.0.0.1, localhost and *.localhost as secure contexts | ||
String frontendHostname = session.getContext().getUri(UrlType.FRONTEND).getRequestUri().getHost(); | ||
if (frontendHostname.equals("127.0.0.1") || frontendHostname.equals("localhost") || frontendHostname.endsWith(".localhost")) { | ||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
} |
34 changes: 34 additions & 0 deletions
34
services/src/main/java/org/keycloak/cookie/DefaultCookieProviderFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package org.keycloak.cookie; | ||
|
||
import org.keycloak.Config; | ||
import org.keycloak.models.KeycloakSession; | ||
import org.keycloak.models.KeycloakSessionFactory; | ||
|
||
public class DefaultCookieProviderFactory implements CookieProviderFactory { | ||
|
||
private boolean legacyCookies; | ||
|
||
@Override | ||
public CookieProvider create(KeycloakSession session) { | ||
return new DefaultCookieProvider(session, legacyCookies); | ||
} | ||
|
||
@Override | ||
public void init(Config.Scope config) { | ||
legacyCookies = config.getBoolean("legacyCookies", false); | ||
} | ||
|
||
@Override | ||
public void postInit(KeycloakSessionFactory factory) { | ||
} | ||
|
||
@Override | ||
public void close() { | ||
} | ||
|
||
@Override | ||
public String getId() { | ||
return "default"; | ||
} | ||
|
||
} |
Oops, something went wrong.