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:
vmuzikar 2020-01-14 09:27:31 +01:00 committed by Bruno Oliveira da Silva
parent 922c9260a4
commit 03306b87e8
15 changed files with 319 additions and 75 deletions

View file

@ -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++)
{

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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");

View file

@ -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;

View file

@ -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);
}
}
}

View file

@ -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();
}

View file

@ -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>

View file

@ -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;

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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
}
}

View file

@ -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)

View file

@ -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));
}