KEYCLOAK-12125 Introduce SameSite attribute in cookies
Co-authored-by: mhajas <mhajas@redhat.com> Co-authored-by: Peter Skopek <pskopek@redhat.com>
This commit is contained in:
parent
922c9260a4
commit
03306b87e8
15 changed files with 319 additions and 75 deletions
|
@ -81,7 +81,16 @@
|
|||
|
||||
function getCookie()
|
||||
{
|
||||
var name = 'KEYCLOAK_SESSION=';
|
||||
var cookie = getCookieByName('KEYCLOAK_SESSION');
|
||||
if (cookie === null) {
|
||||
return getCookieByName('KEYCLOAK_SESSION_LEGACY');
|
||||
}
|
||||
return cookie;
|
||||
}
|
||||
|
||||
function getCookieByName(name)
|
||||
{
|
||||
name = name + '=';
|
||||
var ca = document.cookie.split(';');
|
||||
for(var i=0; i<ca.length; i++)
|
||||
{
|
||||
|
|
|
@ -208,7 +208,7 @@ public class ServletHttpFacade implements HttpFacade {
|
|||
@Override
|
||||
public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly) {
|
||||
StringBuffer cookieBuf = new StringBuffer();
|
||||
ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, null, maxAge, secure, httpOnly);
|
||||
ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, null, maxAge, secure, httpOnly, null);
|
||||
String cookie = cookieBuf.toString();
|
||||
response.addHeader("Set-Cookie", cookie);
|
||||
}
|
||||
|
|
|
@ -219,7 +219,7 @@ public class CatalinaHttpFacade implements HttpFacade {
|
|||
@Override
|
||||
public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly) {
|
||||
StringBuffer cookieBuf = new StringBuffer();
|
||||
ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, null, maxAge, secure, httpOnly);
|
||||
ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, null, maxAge, secure, httpOnly, null);
|
||||
String cookie = cookieBuf.toString();
|
||||
response.addHeader("Set-Cookie", cookie);
|
||||
}
|
||||
|
|
|
@ -32,6 +32,20 @@ public class ServerCookie implements Serializable {
|
|||
private static final String tspecials = ",; ";
|
||||
private static final String tspecials2 = "()<>@,;:\\\"/[]?={} \t";
|
||||
|
||||
public enum SameSiteAttributeValue {
|
||||
NONE("None"); // we currently support only SameSite=None; this might change in the future
|
||||
|
||||
private final String specValue;
|
||||
SameSiteAttributeValue(String specValue) {
|
||||
this.specValue = specValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.lang.String toString() {
|
||||
return specValue;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Tests a string and returns true if the string counts as a
|
||||
* reserved token in the Java language.
|
||||
|
@ -173,7 +187,8 @@ public class ServerCookie implements Serializable {
|
|||
String comment,
|
||||
int maxAge,
|
||||
boolean isSecure,
|
||||
boolean httpOnly) {
|
||||
boolean httpOnly,
|
||||
SameSiteAttributeValue sameSite) {
|
||||
StringBuffer buf = new StringBuffer();
|
||||
// Servlet implementation checks name
|
||||
buf.append(name);
|
||||
|
@ -228,6 +243,12 @@ public class ServerCookie implements Serializable {
|
|||
buf.append(path);
|
||||
}
|
||||
|
||||
// SameSite
|
||||
if (sameSite != null) {
|
||||
buf.append("; SameSite=");
|
||||
buf.append(sameSite.toString());
|
||||
}
|
||||
|
||||
// Secure
|
||||
if (isSecure) {
|
||||
buf.append("; Secure");
|
||||
|
|
|
@ -96,7 +96,8 @@ import java.util.Optional;
|
|||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint.LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX;
|
||||
import static org.keycloak.common.util.ServerCookie.SameSiteAttributeValue;
|
||||
import static org.keycloak.services.util.CookieHelper.getCookie;
|
||||
|
||||
/**
|
||||
* Stateless object that manages authentication
|
||||
|
@ -169,7 +170,7 @@ public class AuthenticationManager {
|
|||
public static void expireUserSessionCookie(KeycloakSession session, UserSessionModel userSession, RealmModel realm, UriInfo uriInfo, HttpHeaders headers, ClientConnection connection) {
|
||||
try {
|
||||
// check to see if any identity cookie is set with the same session and expire it if necessary
|
||||
Cookie cookie = headers.getCookies().get(KEYCLOAK_IDENTITY_COOKIE);
|
||||
Cookie cookie = CookieHelper.getCookie(headers.getCookies(), KEYCLOAK_IDENTITY_COOKIE);
|
||||
if (cookie == null) return;
|
||||
String tokenString = cookie.getValue();
|
||||
|
||||
|
@ -621,7 +622,7 @@ public class AuthenticationManager {
|
|||
maxAge = realm.getSsoSessionMaxLifespanRememberMe() > 0 ? realm.getSsoSessionMaxLifespanRememberMe() : realm.getSsoSessionMaxLifespan();
|
||||
}
|
||||
logger.debugv("Create login cookie - name: {0}, path: {1}, max-age: {2}", KEYCLOAK_IDENTITY_COOKIE, cookiePath, maxAge);
|
||||
CookieHelper.addCookie(KEYCLOAK_IDENTITY_COOKIE, encoded, cookiePath, null, null, maxAge, secureOnly, true);
|
||||
CookieHelper.addCookie(KEYCLOAK_IDENTITY_COOKIE, encoded, cookiePath, null, null, maxAge, secureOnly, true, SameSiteAttributeValue.NONE);
|
||||
//builder.cookie(new NewCookie(cookieName, encoded, cookiePath, null, null, maxAge, secureOnly));// todo httponly , true);
|
||||
|
||||
String sessionCookieValue = realm.getName() + "/" + user.getId();
|
||||
|
@ -631,7 +632,7 @@ public class AuthenticationManager {
|
|||
// THIS SHOULD NOT BE A HTTPONLY COOKIE! It is used for OpenID Connect Iframe Session support!
|
||||
// Max age should be set to the max lifespan of the session as it's used to invalidate old-sessions on re-login
|
||||
int sessionCookieMaxAge = session.isRememberMe() && realm.getSsoSessionMaxLifespanRememberMe() > 0 ? realm.getSsoSessionMaxLifespanRememberMe() : realm.getSsoSessionMaxLifespan();
|
||||
CookieHelper.addCookie(KEYCLOAK_SESSION_COOKIE, sessionCookieValue, cookiePath, null, null, sessionCookieMaxAge, secureOnly, false);
|
||||
CookieHelper.addCookie(KEYCLOAK_SESSION_COOKIE, sessionCookieValue, cookiePath, null, null, sessionCookieMaxAge, secureOnly, false, SameSiteAttributeValue.NONE);
|
||||
P3PHelper.addP3PHeader();
|
||||
}
|
||||
|
||||
|
@ -660,19 +661,19 @@ public class AuthenticationManager {
|
|||
public static void expireIdentityCookie(RealmModel realm, UriInfo uriInfo, ClientConnection connection) {
|
||||
logger.debug("Expiring identity cookie");
|
||||
String path = getIdentityCookiePath(realm, uriInfo);
|
||||
expireCookie(realm, KEYCLOAK_IDENTITY_COOKIE, path, true, connection);
|
||||
expireCookie(realm, KEYCLOAK_SESSION_COOKIE, path, false, connection);
|
||||
expireCookie(realm, KEYCLOAK_IDENTITY_COOKIE, path, true, connection, SameSiteAttributeValue.NONE);
|
||||
expireCookie(realm, KEYCLOAK_SESSION_COOKIE, path, false, connection, SameSiteAttributeValue.NONE);
|
||||
|
||||
String oldPath = getOldCookiePath(realm, uriInfo);
|
||||
expireCookie(realm, KEYCLOAK_IDENTITY_COOKIE, oldPath, true, connection);
|
||||
expireCookie(realm, KEYCLOAK_SESSION_COOKIE, oldPath, false, connection);
|
||||
expireCookie(realm, KEYCLOAK_IDENTITY_COOKIE, oldPath, true, connection, SameSiteAttributeValue.NONE);
|
||||
expireCookie(realm, KEYCLOAK_SESSION_COOKIE, oldPath, false, connection, SameSiteAttributeValue.NONE);
|
||||
}
|
||||
public static void expireOldIdentityCookie(RealmModel realm, UriInfo uriInfo, ClientConnection connection) {
|
||||
logger.debug("Expiring old identity cookie with wrong path");
|
||||
|
||||
String oldPath = getOldCookiePath(realm, uriInfo);
|
||||
expireCookie(realm, KEYCLOAK_IDENTITY_COOKIE, oldPath, true, connection);
|
||||
expireCookie(realm, KEYCLOAK_SESSION_COOKIE, oldPath, false, connection);
|
||||
expireCookie(realm, KEYCLOAK_IDENTITY_COOKIE, oldPath, true, connection, SameSiteAttributeValue.NONE);
|
||||
expireCookie(realm, KEYCLOAK_SESSION_COOKIE, oldPath, false, connection, SameSiteAttributeValue.NONE);
|
||||
}
|
||||
|
||||
|
||||
|
@ -680,14 +681,14 @@ public class AuthenticationManager {
|
|||
logger.debug("Expiring remember me cookie");
|
||||
String path = getIdentityCookiePath(realm, uriInfo);
|
||||
String cookieName = KEYCLOAK_REMEMBER_ME;
|
||||
expireCookie(realm, cookieName, path, true, connection);
|
||||
expireCookie(realm, cookieName, path, true, connection, null);
|
||||
}
|
||||
|
||||
public static void expireOldAuthSessionCookie(RealmModel realm, UriInfo uriInfo, ClientConnection connection) {
|
||||
logger.debugv("Expire {1} cookie .", AuthenticationSessionManager.AUTH_SESSION_ID);
|
||||
|
||||
String oldPath = getOldCookiePath(realm, uriInfo);
|
||||
expireCookie(realm, AuthenticationSessionManager.AUTH_SESSION_ID, oldPath, true, connection);
|
||||
expireCookie(realm, AuthenticationSessionManager.AUTH_SESSION_ID, oldPath, true, connection, null);
|
||||
}
|
||||
|
||||
protected static String getIdentityCookiePath(RealmModel realm, UriInfo uriInfo) {
|
||||
|
@ -710,10 +711,10 @@ public class AuthenticationManager {
|
|||
return uri.getRawPath();
|
||||
}
|
||||
|
||||
public static void expireCookie(RealmModel realm, String cookieName, String path, boolean httpOnly, ClientConnection connection) {
|
||||
public static void expireCookie(RealmModel realm, String cookieName, String path, boolean httpOnly, ClientConnection connection, SameSiteAttributeValue sameSite) {
|
||||
logger.debugf("Expiring cookie: %s path: %s", cookieName, path);
|
||||
boolean secureOnly = realm.getSslRequired().isRequired(connection);;
|
||||
CookieHelper.addCookie(cookieName, "", path, null, "Expiring cookie", 0, secureOnly, httpOnly);
|
||||
CookieHelper.addCookie(cookieName, "", path, null, "Expiring cookie", 0, secureOnly, httpOnly, sameSite);
|
||||
}
|
||||
|
||||
public AuthResult authenticateIdentityCookie(KeycloakSession session, RealmModel realm) {
|
||||
|
@ -721,7 +722,7 @@ public class AuthenticationManager {
|
|||
}
|
||||
|
||||
public static AuthResult authenticateIdentityCookie(KeycloakSession session, RealmModel realm, boolean checkActive) {
|
||||
Cookie cookie = session.getContext().getRequestHeaders().getCookies().get(KEYCLOAK_IDENTITY_COOKIE);
|
||||
Cookie cookie = CookieHelper.getCookie(session.getContext().getRequestHeaders().getCookies(), KEYCLOAK_IDENTITY_COOKIE);
|
||||
if (cookie == null || "".equals(cookie.getValue())) {
|
||||
logger.debugv("Could not find cookie: {0}", KEYCLOAK_IDENTITY_COOKIE);
|
||||
return null;
|
||||
|
@ -756,7 +757,7 @@ public class AuthenticationManager {
|
|||
ClientSessionContext clientSessionCtx,
|
||||
HttpRequest request, UriInfo uriInfo, ClientConnection clientConnection,
|
||||
EventBuilder event, AuthenticationSessionModel authSession, LoginProtocol protocol) {
|
||||
Cookie sessionCookie = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_SESSION_COOKIE);
|
||||
Cookie sessionCookie = getCookie(request.getHttpHeaders().getCookies(), AuthenticationManager.KEYCLOAK_SESSION_COOKIE);
|
||||
if (sessionCookie != null) {
|
||||
|
||||
String[] split = sessionCookie.getValue().split("/");
|
||||
|
@ -801,7 +802,7 @@ public class AuthenticationManager {
|
|||
}
|
||||
|
||||
public static String getSessionIdFromSessionCookie(KeycloakSession session) {
|
||||
Cookie cookie = session.getContext().getRequestHeaders().getCookies().get(KEYCLOAK_SESSION_COOKIE);
|
||||
Cookie cookie = getCookie(session.getContext().getRequestHeaders().getCookies(), KEYCLOAK_SESSION_COOKIE);
|
||||
if (cookie == null || "".equals(cookie.getValue())) {
|
||||
logger.debugv("Could not find cookie: {0}", KEYCLOAK_SESSION_COOKIE);
|
||||
return null;
|
||||
|
|
|
@ -17,12 +17,6 @@
|
|||
|
||||
package org.keycloak.services.util;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.spi.HttpResponse;
|
||||
import org.keycloak.common.util.Resteasy;
|
||||
|
@ -30,6 +24,14 @@ import org.keycloak.common.util.ServerCookie;
|
|||
|
||||
import javax.ws.rs.core.Cookie;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.keycloak.common.util.ServerCookie.SameSiteAttributeValue;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -38,11 +40,46 @@ import javax.ws.rs.core.HttpHeaders;
|
|||
*/
|
||||
public class CookieHelper {
|
||||
|
||||
public static final String LEGACY_COOKIE = "_LEGACY";
|
||||
|
||||
private static final Logger logger = Logger.getLogger(CookieHelper.class);
|
||||
|
||||
/**
|
||||
* Set a response cookie. This solely exists because JAX-RS 1.1 does not support setting HttpOnly cookies
|
||||
*
|
||||
* @param name
|
||||
* @param value
|
||||
* @param path
|
||||
* @param domain
|
||||
* @param comment
|
||||
* @param maxAge
|
||||
* @param secure
|
||||
* @param httpOnly
|
||||
* @param sameSite
|
||||
*/
|
||||
public static void addCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly, SameSiteAttributeValue sameSite) {
|
||||
SameSiteAttributeValue sameSiteParam = sameSite;
|
||||
// when expiring a cookie we shouldn't set the sameSite attribute; if we set e.g. SameSite=None when expiring a cookie, the new cookie (with maxAge == 0)
|
||||
// might be rejected by the browser in some cases resulting in leaving the original cookie untouched; that can even prevent user from accessing their application
|
||||
if (maxAge == 0) {
|
||||
sameSite = null;
|
||||
}
|
||||
|
||||
boolean secure_sameSite = sameSite == SameSiteAttributeValue.NONE || secure; // when SameSite=None, Secure attribute must be set
|
||||
|
||||
HttpResponse response = Resteasy.getContextData(HttpResponse.class);
|
||||
StringBuffer cookieBuf = new StringBuffer();
|
||||
ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, comment, maxAge, secure_sameSite, httpOnly, sameSite);
|
||||
String cookie = cookieBuf.toString();
|
||||
response.getOutputHeaders().add(HttpHeaders.SET_COOKIE, cookie);
|
||||
|
||||
// a workaround for browser in older Apple OSs – browsers ignore cookies with SameSite=None
|
||||
if (sameSiteParam == SameSiteAttributeValue.NONE) {
|
||||
addCookie(name + LEGACY_COOKIE, value, path, domain, comment, maxAge, secure, httpOnly, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a response cookie avoiding SameSite parameter
|
||||
* @param name
|
||||
* @param value
|
||||
* @param path
|
||||
|
@ -53,11 +90,7 @@ public class CookieHelper {
|
|||
* @param httpOnly
|
||||
*/
|
||||
public static void addCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) {
|
||||
HttpResponse response = Resteasy.getContextData(HttpResponse.class);
|
||||
StringBuffer cookieBuf = new StringBuffer();
|
||||
ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, comment, maxAge, secure, httpOnly);
|
||||
String cookie = cookieBuf.toString();
|
||||
response.getOutputHeaders().add(HttpHeaders.SET_COOKIE, cookie);
|
||||
addCookie(name, value, path, domain, comment, maxAge, secure, httpOnly, null);
|
||||
}
|
||||
|
||||
|
||||
|
@ -93,4 +126,16 @@ public class CookieHelper {
|
|||
|
||||
return cookies;
|
||||
}
|
||||
|
||||
public static Cookie getCookie(Map<String, Cookie> cookies, String name) {
|
||||
Cookie cookie = cookies.get(name);
|
||||
if (cookie != null) {
|
||||
return cookie;
|
||||
}
|
||||
else {
|
||||
String legacy = name + LEGACY_COOKIE;
|
||||
logger.debugv("Couldn't find cookie {0}, trying {0}", name, legacy);
|
||||
return cookies.get(legacy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@ import org.keycloak.representations.idm.UserRepresentation;
|
|||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.resource.RealmResourceProvider;
|
||||
import org.keycloak.services.scheduled.ClearExpiredUserSessions;
|
||||
import org.keycloak.services.util.CookieHelper;
|
||||
import org.keycloak.storage.UserStorageProvider;
|
||||
import org.keycloak.testsuite.components.TestProvider;
|
||||
import org.keycloak.testsuite.components.TestProviderFactory;
|
||||
|
@ -559,7 +560,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public String getSSOCookieValue() {
|
||||
Map<String, Cookie> cookies = request.getHttpHeaders().getCookies();
|
||||
Cookie cookie = cookies.get(AuthenticationManager.KEYCLOAK_IDENTITY_COOKIE);
|
||||
Cookie cookie = CookieHelper.getCookie(cookies, AuthenticationManager.KEYCLOAK_IDENTITY_COOKIE);
|
||||
if (cookie == null) return null;
|
||||
return cookie.getValue();
|
||||
}
|
||||
|
|
|
@ -43,5 +43,6 @@
|
|||
<module name="org.hibernate"/>
|
||||
<module name="org.javassist"/>
|
||||
<module name="org.jboss.modules"/>
|
||||
<module name="org.apache.httpcomponents.core"/>
|
||||
</dependencies>
|
||||
</module>
|
||||
|
|
|
@ -74,6 +74,11 @@ public class ClientAttributeUpdater extends ServerResourceUpdater<ClientAttribut
|
|||
return this;
|
||||
}
|
||||
|
||||
public ClientAttributeUpdater setRedirectUris(List<String> values) {
|
||||
this.rep.setRedirectUris(values);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientAttributeUpdater removeAttribute(String name) {
|
||||
this.rep.getAttributes().remove(name);
|
||||
return this;
|
||||
|
|
|
@ -50,6 +50,11 @@ public class JSObjectBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public JSObjectBuilder disableCheckLoginIframe() {
|
||||
arguments.put("checkLoginIframe", false);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JSObjectBuilder loginRequiredOnLoad() {
|
||||
arguments.put("onLoad", "login-required");
|
||||
return this;
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
package org.keycloak.testsuite.adapter.servlet;
|
||||
|
||||
import org.jboss.arquillian.container.test.api.Deployment;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.jboss.shrinkwrap.api.spec.WebArchive;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.adapters.rotation.PublicKeyLocator;
|
||||
import org.keycloak.testsuite.adapter.filter.AdapterActionsFilter;
|
||||
import org.keycloak.testsuite.adapter.page.Employee2Servlet;
|
||||
import org.keycloak.testsuite.adapter.page.EmployeeSigServlet;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
|
||||
import org.keycloak.testsuite.utils.arquillian.ContainerConstants;
|
||||
import org.openqa.selenium.By;
|
||||
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.getAppServerContextRoot;
|
||||
import static org.keycloak.testsuite.auth.page.AuthRealm.SAMLSERVLETDEMO;
|
||||
import static org.keycloak.testsuite.saml.AbstractSamlTest.SAML_CLIENT_ID_EMPLOYEE_2;
|
||||
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
|
||||
import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad;
|
||||
import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
|
||||
|
||||
/**
|
||||
* @author mhajas
|
||||
*/
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_UNDERTOW)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY_DEPRECATED)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_EAP)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_EAP6)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_EAP71)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_TOMCAT7)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_TOMCAT8)
|
||||
@AppServerContainer(ContainerConstants.APP_SERVER_TOMCAT9)
|
||||
@AuthServerContainerExclude(AuthServerContainerExclude.AuthServer.REMOTE)
|
||||
public class SAMLSameSiteTest extends AbstractSAMLServletAdapterTest {
|
||||
private static final String NIP_IO_URL = "app-saml-127-0-0-1.nip.io";
|
||||
private static final String NIP_IO_EMPLOYEE2_URL = getAppServerContextRoot().replace("localhost", NIP_IO_URL) + "/employee2/";
|
||||
|
||||
@Deployment(name = Employee2Servlet.DEPLOYMENT_NAME)
|
||||
protected static WebArchive employee2() {
|
||||
return samlServletDeployment(Employee2Servlet.DEPLOYMENT_NAME, WEB_XML_WITH_ACTION_FILTER, SendUsernameServlet.class, AdapterActionsFilter.class, PublicKeyLocator.class);
|
||||
}
|
||||
|
||||
@Page
|
||||
protected Employee2Servlet employee2ServletPage;
|
||||
|
||||
@Test
|
||||
public void samlWorksWithSameSiteCookieTest() throws URISyntaxException {
|
||||
getCleanup(SAMLSERVLETDEMO).addCleanup(ClientAttributeUpdater.forClient(adminClient, SAMLSERVLETDEMO, SAML_CLIENT_ID_EMPLOYEE_2)
|
||||
.setRedirectUris(Collections.singletonList(NIP_IO_EMPLOYEE2_URL + "*"))
|
||||
.setAdminUrl(NIP_IO_EMPLOYEE2_URL + "saml")
|
||||
.update());
|
||||
|
||||
// Navigate to url with nip.io to trick browser the adapter lives on different domain
|
||||
driver.navigate().to(NIP_IO_EMPLOYEE2_URL);
|
||||
assertCurrentUrlStartsWith(testRealmSAMLPostLoginPage);
|
||||
|
||||
// Login and check the user is successfully logged in
|
||||
testRealmSAMLPostLoginPage.form().login(bburkeUser);
|
||||
waitUntilElement(By.xpath("//body")).text().contains("principal=bburke@redhat.com");
|
||||
|
||||
// Logout
|
||||
driver.navigate().to(UriBuilder.fromUri(NIP_IO_EMPLOYEE2_URL).queryParam("GLO", "true").build().toASCIIString());
|
||||
waitForPageToLoad();
|
||||
|
||||
// Check logged out
|
||||
driver.navigate().to(NIP_IO_EMPLOYEE2_URL);
|
||||
assertCurrentUrlStartsWith(testRealmSAMLPostLoginPage);
|
||||
}
|
||||
}
|
|
@ -17,6 +17,13 @@
|
|||
|
||||
package org.keycloak.testsuite.admin;
|
||||
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.client.methods.RequestBuilder;
|
||||
import org.apache.http.impl.client.BasicCookieStore;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
|
||||
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
|
||||
|
@ -49,6 +56,8 @@ import org.keycloak.services.managers.AuthenticationManager;
|
|||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
||||
import org.keycloak.testsuite.auth.page.AuthRealm;
|
||||
import org.keycloak.testsuite.pages.AppPage;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
|
@ -61,18 +70,16 @@ import org.keycloak.testsuite.util.UserBuilder;
|
|||
import org.openqa.selenium.Cookie;
|
||||
|
||||
import javax.ws.rs.ClientErrorException;
|
||||
import javax.ws.rs.client.Client;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.NewCookie;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
||||
import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT;
|
||||
|
||||
/**
|
||||
* Tests Undertow Adapter
|
||||
|
@ -238,21 +245,19 @@ public class ImpersonationTest extends AbstractKeycloakTest {
|
|||
}
|
||||
|
||||
private Cookie impersonate(Keycloak adminClient, String admin, String adminRealm) {
|
||||
Client httpClient = javax.ws.rs.client.ClientBuilder.newClient();
|
||||
BasicCookieStore cookieStore = new BasicCookieStore();
|
||||
try (CloseableHttpClient httpClient = HttpClientBuilder.create().setDefaultCookieStore(cookieStore).build()) {
|
||||
|
||||
try (Response response = httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
|
||||
.path("admin")
|
||||
.path("realms")
|
||||
.path("test")
|
||||
.path("users/" + impersonatedUserId + "/impersonation")
|
||||
.request()
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + adminClient.tokenManager().getAccessTokenString())
|
||||
.post(null)) {
|
||||
HttpUriRequest req = RequestBuilder.post()
|
||||
.setUri(AUTH_SERVER_ROOT + "/admin/realms/test/users/" + impersonatedUserId + "/impersonation")
|
||||
.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + adminClient.tokenManager().getAccessTokenString())
|
||||
.build();
|
||||
|
||||
Map data = response.readEntity(Map.class);
|
||||
HttpResponse res = httpClient.execute(req);
|
||||
String resBody = EntityUtils.toString(res.getEntity());
|
||||
|
||||
Assert.assertNotNull(data);
|
||||
Assert.assertNotNull(data.get("redirect"));
|
||||
Assert.assertNotNull(resBody);
|
||||
Assert.assertTrue(resBody.contains("redirect"));
|
||||
|
||||
events.expect(EventType.IMPERSONATE)
|
||||
.session(AssertEvents.isUUID())
|
||||
|
@ -275,10 +280,15 @@ public class ImpersonationTest extends AbstractKeycloakTest {
|
|||
Assert.assertNotNull(notes.get(ImpersonationSessionNote.IMPERSONATOR_ID.toString()));
|
||||
Assert.assertEquals(admin, notes.get(ImpersonationSessionNote.IMPERSONATOR_USERNAME.toString()));
|
||||
|
||||
NewCookie cookie = response.getCookies().get(AuthenticationManager.KEYCLOAK_IDENTITY_COOKIE);
|
||||
org.apache.http.cookie.Cookie cookie = cookieStore.getCookies().stream()
|
||||
.filter(c -> c.getName().equals(AuthenticationManager.KEYCLOAK_IDENTITY_COOKIE))
|
||||
.findAny().orElse(null);
|
||||
Assert.assertNotNull(cookie);
|
||||
|
||||
return new Cookie(cookie.getName(), cookie.getValue(), cookie.getDomain(), cookie.getPath(), cookie.getExpiry(), cookie.isSecure(), cookie.isHttpOnly());
|
||||
return new Cookie(cookie.getName(), cookie.getValue(), cookie.getDomain(), cookie.getPath(), cookie.getExpiryDate(), cookie.isSecure(), true);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,15 +16,6 @@
|
|||
*/
|
||||
package org.keycloak.testsuite.cookies;
|
||||
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||
import org.keycloak.testsuite.auth.page.AuthRealm;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.OAuthClient.AuthorizationEndpointResponse;
|
||||
import org.keycloak.testsuite.util.RealmBuilder;
|
||||
import java.util.List;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.protocol.HttpClientContext;
|
||||
|
@ -36,14 +27,34 @@ import org.apache.http.protocol.HttpContext;
|
|||
import org.apache.http.util.EntityUtils;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||
import org.keycloak.testsuite.auth.page.AuthRealm;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.OAuthClient.AuthorizationEndpointResponse;
|
||||
import org.keycloak.testsuite.util.RealmBuilder;
|
||||
import org.openqa.selenium.Cookie;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.keycloak.services.managers.AuthenticationManager.KEYCLOAK_IDENTITY_COOKIE;
|
||||
import static org.keycloak.services.managers.AuthenticationManager.KEYCLOAK_SESSION_COOKIE;
|
||||
import static org.keycloak.services.util.CookieHelper.LEGACY_COOKIE;
|
||||
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
||||
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author hmlnarik
|
||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||
*/
|
||||
public class CookieTest extends AbstractKeycloakTest {
|
||||
|
||||
|
@ -58,10 +69,23 @@ public class CookieTest extends AbstractKeycloakTest {
|
|||
testRealms.add(testRealm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDefaultPageUriParameters() {
|
||||
super.setDefaultPageUriParameters();
|
||||
accountPage.setAuthRealm(AuthRealm.TEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCookieValue() throws Exception {
|
||||
accountPage.setAuthRealm(AuthRealm.TEST);
|
||||
testCookieValue(KEYCLOAK_IDENTITY_COOKIE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLegacyCookieValue() throws Exception {
|
||||
testCookieValue(KEYCLOAK_IDENTITY_COOKIE + LEGACY_COOKIE);
|
||||
}
|
||||
|
||||
private void testCookieValue(String cookieName) throws Exception {
|
||||
final String accountClientId = realmsResouce().realm("test").clients().findByClientId("account").get(0).getId();
|
||||
final String clientSecret = realmsResouce().realm("test").clients().get(accountClientId).getSecret().getValue();
|
||||
|
||||
|
@ -74,7 +98,7 @@ public class CookieTest extends AbstractKeycloakTest {
|
|||
|
||||
try (CloseableHttpClient hc = OAuthClient.newCloseableHttpClient()) {
|
||||
BasicCookieStore cookieStore = new BasicCookieStore();
|
||||
BasicClientCookie cookie = new BasicClientCookie(AuthenticationManager.KEYCLOAK_IDENTITY_COOKIE, accessToken);
|
||||
BasicClientCookie cookie = new BasicClientCookie(cookieName, accessToken);
|
||||
cookie.setDomain("localhost");
|
||||
cookie.setPath("/");
|
||||
cookieStore.addCookie(cookie);
|
||||
|
@ -99,8 +123,6 @@ public class CookieTest extends AbstractKeycloakTest {
|
|||
|
||||
@Test
|
||||
public void testCookieValueLoggedOut() throws Exception {
|
||||
accountPage.setAuthRealm(AuthRealm.TEST);
|
||||
|
||||
final String accountClientId = realmsResouce().realm("test").clients().findByClientId("account").get(0).getId();
|
||||
final String clientSecret = realmsResouce().realm("test").clients().get(accountClientId).getSecret().getValue();
|
||||
|
||||
|
@ -114,7 +136,7 @@ public class CookieTest extends AbstractKeycloakTest {
|
|||
|
||||
try (CloseableHttpClient hc = OAuthClient.newCloseableHttpClient()) {
|
||||
BasicCookieStore cookieStore = new BasicCookieStore();
|
||||
BasicClientCookie cookie = new BasicClientCookie(AuthenticationManager.KEYCLOAK_IDENTITY_COOKIE, accessToken);
|
||||
BasicClientCookie cookie = new BasicClientCookie(KEYCLOAK_IDENTITY_COOKIE, accessToken);
|
||||
cookie.setDomain("localhost");
|
||||
cookie.setPath("/");
|
||||
cookieStore.addCookie(cookie);
|
||||
|
@ -137,4 +159,34 @@ public class CookieTest extends AbstractKeycloakTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void legacyCookiesTest() {
|
||||
accountPage.navigateTo();
|
||||
assertCurrentUrlStartsWithLoginUrlOf(accountPage);
|
||||
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
|
||||
Cookie sameSiteIdentityCookie = driver.manage().getCookieNamed(KEYCLOAK_IDENTITY_COOKIE);
|
||||
Cookie legacyIdentityCookie = driver.manage().getCookieNamed(KEYCLOAK_IDENTITY_COOKIE + LEGACY_COOKIE);
|
||||
Cookie sameSiteSessionCookie = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE);
|
||||
Cookie legacySessionCookie = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE + LEGACY_COOKIE);
|
||||
|
||||
assertSameSiteCookies(sameSiteIdentityCookie, legacyIdentityCookie);
|
||||
assertSameSiteCookies(sameSiteSessionCookie, legacySessionCookie);
|
||||
}
|
||||
|
||||
private void assertSameSiteCookies(Cookie sameSiteCookie, Cookie legacyCookie) {
|
||||
assertNotNull("SameSite cookie shouldn't be null", sameSiteCookie);
|
||||
assertNotNull("Legacy cookie shouldn't be null", legacyCookie);
|
||||
|
||||
assertEquals(sameSiteCookie.getValue(), legacyCookie.getValue());
|
||||
assertEquals(sameSiteCookie.getDomain(), legacyCookie.getDomain());
|
||||
assertEquals(sameSiteCookie.getPath(), legacyCookie.getPath());
|
||||
assertEquals(sameSiteCookie.getExpiry(), legacyCookie.getExpiry());
|
||||
assertTrue("SameSite cookie should always have Secure attribute", sameSiteCookie.isSecure());
|
||||
assertFalse("Legacy cookie shouldn't have Secure attribute", legacyCookie.isSecure()); // this relies on test realm config
|
||||
assertEquals(sameSiteCookie.isHttpOnly(), legacyCookie.isHttpOnly());
|
||||
// WebDriver currently doesn't support SameSite attribute therefore we cannot check it's present in the cookie
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ public abstract class AbstractJavascriptTest extends AbstractAuthTest {
|
|||
void apply(T a, U b, V c, W d);
|
||||
}
|
||||
|
||||
public static final String NIP_IO_URL = "js-app-127-0-0-1.nip.io";
|
||||
public static final String CLIENT_ID = "js-console";
|
||||
public static final String REALM_NAME = "test";
|
||||
public static final String SPACE_REALM_NAME = "Example realm";
|
||||
|
@ -112,7 +113,8 @@ public abstract class AbstractJavascriptTest extends AbstractAuthTest {
|
|||
.client(
|
||||
ClientBuilder.create()
|
||||
.clientId(CLIENT_ID)
|
||||
.redirectUris(oauth.SERVER_ROOT + JAVASCRIPT_URL + "/*", oauth.SERVER_ROOT + JAVASCRIPT_ENCODED_SPACE_URL + "/*")
|
||||
.redirectUris(oauth.SERVER_ROOT.replace("localhost", NIP_IO_URL) + JAVASCRIPT_URL + "/*", oauth.SERVER_ROOT + JAVASCRIPT_ENCODED_SPACE_URL + "/*")
|
||||
.addWebOrigin(oauth.SERVER_ROOT.replace("localhost", NIP_IO_URL))
|
||||
.publicClient()
|
||||
)
|
||||
.accessTokenLifespan(30 + TOKEN_LIFESPAN_LEEWAY)
|
||||
|
|
|
@ -38,6 +38,7 @@ import java.util.stream.Collectors;
|
|||
import java.util.stream.Stream;
|
||||
|
||||
import static java.lang.Math.toIntExact;
|
||||
import static org.hamcrest.CoreMatchers.anyOf;
|
||||
import static org.hamcrest.CoreMatchers.both;
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.hamcrest.Matchers.greaterThan;
|
||||
|
@ -83,12 +84,17 @@ public class JavascriptAdapterTest extends AbstractJavascriptTest {
|
|||
|
||||
@Before
|
||||
public void setDefaultEnvironment() {
|
||||
testAppUrl = authServerContextRootPage + JAVASCRIPT_URL + "/index.html";
|
||||
testAppUrl = authServerContextRootPage.toString().replace("localhost", NIP_IO_URL) + JAVASCRIPT_URL + "/index.html";
|
||||
|
||||
jsDriverTestRealmLoginPage.setAuthRealm(REALM_NAME);
|
||||
oAuthGrantPage.setAuthRealm(REALM_NAME);
|
||||
applicationsPage.setAuthRealm(REALM_NAME);
|
||||
|
||||
jsDriver.navigate().to(oauth.getLoginFormUrl());
|
||||
waitForPageToLoad();
|
||||
events.poll();
|
||||
jsDriver.manage().deleteAllCookies();
|
||||
|
||||
jsDriver.navigate().to(testAppUrl);
|
||||
|
||||
waitUntilElement(outputArea).is().present();
|
||||
|
@ -141,8 +147,15 @@ public class JavascriptAdapterTest extends AbstractJavascriptTest {
|
|||
.login(this::assertOnLoginPage)
|
||||
.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(defaultArguments(), this::assertSuccessfullyLoggedIn)
|
||||
.logout(this::assertOnTestAppUrl)
|
||||
|
||||
.init(defaultArguments(), this::assertInitNotAuth)
|
||||
.login("{kcLocale: 'de'}", assertLocaleIsSet("de"))
|
||||
.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(defaultArguments(), this::assertSuccessfullyLoggedIn)
|
||||
.logout(this::assertOnTestAppUrl)
|
||||
|
||||
.init(defaultArguments(), this::assertInitNotAuth)
|
||||
.login("{kcLocale: 'en'}", assertLocaleIsSet("en"));
|
||||
}
|
||||
|
||||
|
@ -166,7 +179,7 @@ public class JavascriptAdapterTest extends AbstractJavascriptTest {
|
|||
.init(checkSSO, this::assertSuccessfullyLoggedIn)
|
||||
.refresh()
|
||||
.init(checkSSO
|
||||
.add("silentCheckSsoRedirectUri", authServerContextRootPage + JAVASCRIPT_URL + "/silent-check-sso.html")
|
||||
.add("silentCheckSsoRedirectUri", authServerContextRootPage.toString().replace("localhost", NIP_IO_URL) + JAVASCRIPT_URL + "/silent-check-sso.html")
|
||||
, this::assertSuccessfullyLoggedIn);
|
||||
}
|
||||
|
||||
|
@ -179,8 +192,8 @@ public class JavascriptAdapterTest extends AbstractJavascriptTest {
|
|||
.init(checkSSO, this::assertSuccessfullyLoggedIn)
|
||||
.refresh()
|
||||
.init(checkSSO
|
||||
.add("checkLoginIframe", false)
|
||||
.add("silentCheckSsoRedirectUri", authServerContextRootPage + JAVASCRIPT_URL + "/silent-check-sso.html")
|
||||
.disableCheckLoginIframe()
|
||||
.add("silentCheckSsoRedirectUri", authServerContextRootPage.toString().replace("localhost", NIP_IO_URL) + JAVASCRIPT_URL + "/silent-check-sso.html")
|
||||
, this::assertSuccessfullyLoggedIn);
|
||||
}
|
||||
|
||||
|
@ -205,7 +218,7 @@ public class JavascriptAdapterTest extends AbstractJavascriptTest {
|
|||
JSObjectBuilder checkSSO = defaultArguments().checkSSOOnLoad();
|
||||
testExecutor.init(checkSSO
|
||||
.add("checkLoginIframe", false)
|
||||
.add("silentCheckSsoRedirectUri", authServerContextRootPage + JAVASCRIPT_URL + "/silent-check-sso.html")
|
||||
.add("silentCheckSsoRedirectUri", authServerContextRootPage.toString().replace("localhost", NIP_IO_URL) + JAVASCRIPT_URL + "/silent-check-sso.html")
|
||||
, this::assertInitNotAuth);
|
||||
}
|
||||
|
||||
|
@ -378,7 +391,8 @@ public class JavascriptAdapterTest extends AbstractJavascriptTest {
|
|||
.addHeader("Authorization", "Bearer ' + keycloak.token + '");
|
||||
|
||||
testExecutor.init(defaultArguments())
|
||||
.sendXMLHttpRequest(request, assertResponseStatus(401))
|
||||
// Possibility of 0 and 401 is caused by this issue: https://issues.redhat.com/browse/KEYCLOAK-12686
|
||||
.sendXMLHttpRequest(request, response -> assertThat(response, hasEntry(is("status"), anyOf(is(0L), is(401L)))))
|
||||
.refresh();
|
||||
if (!"phantomjs".equals(System.getProperty("js.browser"))) {
|
||||
// I have no idea why, but this request doesn't work with phantomjs, it works in chrome
|
||||
|
@ -422,7 +436,8 @@ public class JavascriptAdapterTest extends AbstractJavascriptTest {
|
|||
|
||||
setTimeOffset(67);
|
||||
testExecutor.addTimeSkew(-34)
|
||||
.sendXMLHttpRequest(request, assertResponseStatus(401))
|
||||
// Possibility of 0 and 401 is caused by this issue: https://issues.redhat.com/browse/KEYCLOAK-12686
|
||||
.sendXMLHttpRequest(request, response -> assertThat(response, hasEntry(is("status"), anyOf(is(0L), is(401L)))))
|
||||
.refreshToken(5, assertEventsContains("Auth Refresh Success"))
|
||||
.sendXMLHttpRequest(request, assertResponseStatus(200));
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue