KEYCLOAK-5270 Realm cookie path for IE<=11 users (#5106)

This commit is contained in:
Martin Kanis 2018-04-06 09:26:29 +02:00 committed by Marek Posolda
parent 6339b62f8d
commit f429469fc8
10 changed files with 493 additions and 91 deletions

View file

@ -90,7 +90,7 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
}
private RootAuthenticationSessionEntity getRootAuthenticationSessionEntity(RealmModel realm, String authSessionId) {
private RootAuthenticationSessionEntity getRootAuthenticationSessionEntity(String authSessionId) {
// Chance created in this transaction
RootAuthenticationSessionEntity entity = tx.get(cache, authSessionId);
@ -181,7 +181,7 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
@Override
public RootAuthenticationSessionModel getRootAuthenticationSession(RealmModel realm, String authenticationSessionId) {
RootAuthenticationSessionEntity entity = getRootAuthenticationSessionEntity(realm, authenticationSessionId);
RootAuthenticationSessionEntity entity = getRootAuthenticationSessionEntity(authenticationSessionId);
return wrap(realm, entity);
}

View file

@ -20,6 +20,7 @@ package org.keycloak.sessions;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.Provider;
import java.util.Map;
/**

View file

@ -35,12 +35,9 @@ import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.services.ErrorPageException;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.UserSessionCrossDCManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.services.util.AuthenticationFlowURLHelper;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel;
@ -170,26 +167,25 @@ public abstract class AuthorizationEndpointBase {
protected AuthenticationSessionModel createAuthenticationSession(ClientModel client, String requestState) {
AuthenticationSessionManager manager = new AuthenticationSessionManager(session);
String authSessionId = manager.getCurrentAuthenticationSessionId(realm);
RootAuthenticationSessionModel rootAuthSession = authSessionId==null ? null : session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
RootAuthenticationSessionModel rootAuthSession = manager.getCurrentRootAuthenticationSession(realm);
AuthenticationSessionModel authSession;
if (rootAuthSession != null) {
authSession = rootAuthSession.createAuthenticationSession(client);
logger.debugf("Sent request to authz endpoint. Root authentication session with ID '%s' exists. Client is '%s' . Created new authentication session with tab ID: %s",
rootAuthSession.getId(), client.getClientId(), authSession.getTabId());
} else {
UserSessionModel userSession = authSessionId == null ? null : new UserSessionCrossDCManager(session).getUserSessionIfExistsRemotely(realm, authSessionId);
UserSessionCrossDCManager userSessionCrossDCManager = new UserSessionCrossDCManager(session);
UserSessionModel userSession = userSessionCrossDCManager.getUserSessionIfExistsRemotely(manager, realm);
if (userSession != null) {
rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(authSessionId, realm);
String userSessionId = userSession.getId();
rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(userSessionId, realm);
authSession = rootAuthSession.createAuthenticationSession(client);
logger.debugf("Sent request to authz endpoint. We don't have root authentication session with ID '%s' but we have userSession." +
"Re-created root authentication session with same ID. Client is: %s . New authentication session tab ID: %s", authSessionId, client.getClientId(), authSession.getTabId());
"Re-created root authentication session with same ID. Client is: %s . New authentication session tab ID: %s", userSessionId, client.getClientId(), authSession.getTabId());
} else {
rootAuthSession = manager.createAuthenticationSession(realm, true);
authSession = rootAuthSession.createAuthenticationSession(client);

View file

@ -67,6 +67,7 @@ import java.net.URI;
import java.security.PublicKey;
import java.util.*;
import java.util.stream.Collectors;
import java.util.AbstractMap.SimpleEntry;
/**
* Stateless object that manages authentication
@ -223,20 +224,22 @@ public class AuthenticationManager {
// Account management client is used as a placeholder
ClientModel client = SystemClientUtil.getSystemClient(realm);
// Try to lookup current authSessionId from browser cookie. If doesn't exists, use the same as current userSession
String authSessionId = null;
String authSessionId;
RootAuthenticationSessionModel rootLogoutSession = null;
boolean browserCookiePresent = false;
// Try to lookup current authSessionId from browser cookie. If doesn't exists, use the same as current userSession
if (browserCookie) {
authSessionId = asm.getCurrentAuthenticationSessionId(realm);
rootLogoutSession = asm.getCurrentRootAuthenticationSession(realm);
}
if (authSessionId != null) {
if (rootLogoutSession != null) {
authSessionId = rootLogoutSession.getId();
browserCookiePresent = true;
} else {
authSessionId = userSession.getId();
rootLogoutSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
}
// Try to join existing logout session if it exists
RootAuthenticationSessionModel rootLogoutSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
if (rootLogoutSession == null) {
rootLogoutSession = session.authenticationSessions().createRootAuthenticationSession(authSessionId, realm);
}
@ -615,7 +618,20 @@ public class AuthenticationManager {
String path = getIdentityCookiePath(realm, uriInfo);
expireCookie(realm, KEYCLOAK_IDENTITY_COOKIE, path, true, connection);
expireCookie(realm, KEYCLOAK_SESSION_COOKIE, path, false, connection);
String oldPath = getOldCookiePath(realm, uriInfo);
expireCookie(realm, KEYCLOAK_IDENTITY_COOKIE, oldPath, true, connection);
expireCookie(realm, KEYCLOAK_SESSION_COOKIE, oldPath, false, connection);
}
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);
}
public static void expireRememberMeCookie(RealmModel realm, UriInfo uriInfo, ClientConnection connection) {
logger.debug("Expiring remember me cookie");
String path = getIdentityCookiePath(realm, uriInfo);
@ -623,11 +639,24 @@ public class AuthenticationManager {
expireCookie(realm, cookieName, path, true, connection);
}
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);
}
protected static String getIdentityCookiePath(RealmModel realm, UriInfo uriInfo) {
return getRealmCookiePath(realm, uriInfo);
}
public static String getRealmCookiePath(RealmModel realm, UriInfo uriInfo) {
URI uri = RealmsResource.realmBaseUrl(uriInfo).build(realm.getName());
// KEYCLOAK-5270
return uri.getRawPath() + "/";
}
public static String getOldCookiePath(RealmModel realm, UriInfo uriInfo) {
URI uri = RealmsResource.realmBaseUrl(uriInfo).build(realm.getName());
return uri.getRawPath();
}
@ -658,6 +687,7 @@ public class AuthenticationManager {
AuthResult authResult = verifyIdentityToken(session, realm, session.getContext().getUri(), session.getContext().getConnection(), checkActive, false, true, tokenString, session.getContext().getRequestHeaders());
if (authResult == null) {
expireIdentityCookie(realm, session.getContext().getUri(), session.getContext().getConnection());
expireOldIdentityCookie(realm, session.getContext().getUri(), session.getContext().getConnection());
return null;
}
authResult.getSession().setLastSessionRefresh(Time.currentTime());

View file

@ -17,8 +17,6 @@
package org.keycloak.services.managers;
import javax.ws.rs.core.UriInfo;
import org.jboss.logging.Logger;
import org.keycloak.common.ClientConnection;
import org.keycloak.models.ClientModel;
@ -31,6 +29,13 @@ import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel;
import org.keycloak.sessions.StickySessionEncoderProvider;
import javax.ws.rs.core.UriInfo;
import java.util.AbstractMap.SimpleEntry;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@ -38,6 +43,8 @@ public class AuthenticationSessionManager {
public static final String AUTH_SESSION_ID = "AUTH_SESSION_ID";
public static final int AUTH_SESSION_LIMIT = 3;
private static final Logger log = Logger.getLogger(AuthenticationSessionManager.class);
private final KeycloakSession session;
@ -64,14 +71,40 @@ public class AuthenticationSessionManager {
return rootAuthSession;
}
public RootAuthenticationSessionModel getCurrentRootAuthenticationSession(RealmModel realm) {
List<String> authSessionIds = getAuthSessionCookieIds(realm);
/**
* Returns ID of current authentication session if it exists, otherwise returns {@code null}.
* @param realm
* @return
*/
public String getCurrentAuthenticationSessionId(RealmModel realm) {
return getAuthSessionCookieDecoded(realm);
return authSessionIds.stream().map(id -> {
SimpleEntry<String, String> entry = decodeAuthSessionId(id);
String sessionId = entry.getKey();
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, sessionId);
if (rootAuthSession != null) {
reencodeAuthSessionCookie(sessionId, entry.getValue(), realm);
return rootAuthSession;
}
return null;
}).filter(authSession -> Objects.nonNull(authSession)).findFirst().orElse(null);
}
public UserSessionModel getUserSessionFromAuthCookie(RealmModel realm) {
List<String> authSessionIds = getAuthSessionCookieIds(realm);
return authSessionIds.stream().map(id -> {
SimpleEntry<String, String> entry = decodeAuthSessionId(id);
String sessionId = entry.getKey();
UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
if (userSession != null) {
reencodeAuthSessionCookie(sessionId, entry.getValue(), realm);
return userSession;
}
return null;
}).filter(authSession -> Objects.nonNull(authSession)).findFirst().orElse(null);
}
@ -81,13 +114,21 @@ public class AuthenticationSessionManager {
* @return
*/
public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm, ClientModel client, String tabId) {
String authSessionId = getAuthSessionCookieDecoded(realm);
List<String> authSessionIds = getAuthSessionCookieIds(realm);
if (authSessionId == null) {
return null;
return authSessionIds.stream().map(id -> {
SimpleEntry<String, String> entry = decodeAuthSessionId(id);
String sessionId = entry.getKey();
AuthenticationSessionModel authSession = getAuthenticationSessionByIdAndClient(realm, sessionId, client, tabId);
if (authSession != null) {
reencodeAuthSessionCookie(sessionId, entry.getValue(), realm);
return authSession;
}
return getAuthenticationSessionByIdAndClient(realm, authSessionId, client, tabId);
return null;
}).filter(authSession -> Objects.nonNull(authSession)).findFirst().orElse(null);
}
@ -105,28 +146,36 @@ public class AuthenticationSessionManager {
log.debugf("Set AUTH_SESSION_ID cookie with value %s", encodedAuthSessionId);
}
public String getAuthSessionCookieDecoded(RealmModel realm) {
String cookieVal = CookieHelper.getCookieValue(AUTH_SESSION_ID);
if (cookieVal != null) {
log.debugf("Found AUTH_SESSION_ID cookie with value %s", cookieVal);
public SimpleEntry<String, String> decodeAuthSessionId(String authSessionId) {
log.debugf("Found AUTH_SESSION_ID cookie with value %s", authSessionId);
StickySessionEncoderProvider encoder = session.getProvider(StickySessionEncoderProvider.class);
String decodedAuthSessionId = encoder.decodeSessionId(cookieVal);
// Check if owner of this authentication session changed due to re-hashing (usually node failover or addition of new node)
String decodedAuthSessionId = encoder.decodeSessionId(authSessionId);
String reencoded = encoder.encodeSessionId(decodedAuthSessionId);
if (!reencoded.equals(cookieVal)) {
return new SimpleEntry(decodedAuthSessionId, reencoded);
}
public void reencodeAuthSessionCookie(String decodedAuthSessionId, String reencodedAuthSessionId, RealmModel realm) {
if (!decodedAuthSessionId.equals(reencodedAuthSessionId)) {
log.debugf("Route changed. Will update authentication session cookie");
setAuthSessionCookie(decodedAuthSessionId, realm);
}
return decodedAuthSessionId;
} else {
log.debugf("Not found AUTH_SESSION_ID cookie");
return null;
}
public List<String> getAuthSessionCookieIds(RealmModel realm) {
Set<String> cookiesVal = CookieHelper.getCookieValue(AUTH_SESSION_ID);
if (cookiesVal.size() > 1) {
AuthenticationManager.expireOldAuthSessionCookie(realm, session.getContext().getUri(), session.getContext().getConnection());
}
List<String> authSessionIds = cookiesVal.stream().limit(AUTH_SESSION_LIMIT).collect(Collectors.toList());
if (authSessionIds.isEmpty()) {
log.debugf("Not found AUTH_SESSION_ID cookie");
}
return authSessionIds;
}
@ -156,5 +205,4 @@ public class AuthenticationSessionManager {
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
return rootAuthSession==null ? null : rootAuthSession.getAuthenticationSession(client, tabId);
}
}

View file

@ -17,14 +17,15 @@
package org.keycloak.services.managers;
import java.util.Map;
import java.util.AbstractMap.SimpleEntry;
import java.util.List;
import java.util.Objects;
import org.jboss.logging.Logger;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.sessions.CommonClientSessionModel;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -60,14 +61,24 @@ public class UserSessionCrossDCManager {
// Just check if userSession also exists on remoteCache. It can happen that logout happened on 2nd DC and userSession is already removed on remoteCache and this DC wasn't yet notified
public UserSessionModel getUserSessionIfExistsRemotely(RealmModel realm, String id) {
UserSessionModel userSession = kcSession.sessions().getUserSession(realm, id);
public UserSessionModel getUserSessionIfExistsRemotely(AuthenticationSessionManager asm, RealmModel realm) {
List<String> sessionIds = asm.getAuthSessionCookieIds(realm);
return sessionIds.stream().map(id -> {
SimpleEntry<String, String> entry = asm.decodeAuthSessionId(id);
String sessionId = entry.getKey();
// This will remove userSession "locally" if it doesn't exists on remoteCache
kcSession.sessions().getUserSessionWithPredicate(realm, id, false, (UserSessionModel userSession2) -> {
return userSession2 == null;
});
kcSession.sessions().getUserSessionWithPredicate(realm, sessionId, false, (UserSessionModel userSession2) -> userSession2 == null);
return kcSession.sessions().getUserSession(realm, id);
UserSessionModel userSession = kcSession.sessions().getUserSession(realm, sessionId);
if (userSession != null) {
asm.reencodeAuthSessionCookie(sessionId, entry.getValue(), realm);
return userSession;
}
return null;
}).filter(userSession -> Objects.nonNull(userSession)).findFirst().orElse(null);
}
}

View file

@ -322,7 +322,7 @@ public class LoginActionsService {
@QueryParam(Constants.TAB_ID) String tabId,
@QueryParam(Constants.KEY) String key) {
if (key != null) {
return handleActionToken(authSessionId, key, execution, clientId, tabId);
return handleActionToken(key, execution, clientId, tabId);
}
event.event(EventType.RESET_PASSWORD);
@ -422,10 +422,10 @@ public class LoginActionsService {
@QueryParam("execution") String execution,
@QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) {
return handleActionToken(authSessionId, key, execution, clientId, tabId);
return handleActionToken(key, execution, clientId, tabId);
}
protected <T extends JsonWebToken & ActionTokenKeyModel> Response handleActionToken(String authSessionId, String tokenString, String execution, String clientId, String tabId) {
protected <T extends JsonWebToken & ActionTokenKeyModel> Response handleActionToken(String tokenString, String execution, String clientId, String tabId) {
T token;
ActionTokenHandler<T> handler;
ActionTokenContext<T> tokenContext;
@ -442,7 +442,6 @@ public class LoginActionsService {
AuthenticationSessionManager authenticationSessionManager = new AuthenticationSessionManager(session);
if (client != null) {
session.getContext().setClient(client);
authSessionId = authSessionId == null ? authenticationSessionManager.getAuthSessionCookieDecoded(realm) : authSessionId;
authSession = authenticationSessionManager.getCurrentAuthenticationSession(realm, client, tabId);
}

View file

@ -18,6 +18,10 @@
package org.keycloak.services.resources;
import java.net.URI;
import java.util.AbstractMap.SimpleEntry;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
@ -73,7 +77,6 @@ public class SessionCodeChecks {
private final String flowPath;
private final String authSessionId;
public SessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event,
String authSessionId, String code, String execution, String clientId, String tabId, String flowPath) {
this.realm = realm;
@ -175,12 +178,9 @@ public class SessionCodeChecks {
}
// See if we are already authenticated and userSession with same ID exists.
String sessionId = authSessionManager.getCurrentAuthenticationSessionId(realm);
RootAuthenticationSessionModel existingRootAuthSession = null;
if (sessionId != null) {
UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
if (userSession != null) {
UserSessionModel userSession = authSessionManager.getUserSessionFromAuthCookie(realm);
if (userSession != null) {
LoginFormsProvider loginForm = session.getProvider(LoginFormsProvider.class).setAuthenticationSession(authSession)
.setSuccess(Messages.ALREADY_LOGGED_IN);
@ -192,11 +192,8 @@ public class SessionCodeChecks {
return null;
}
existingRootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, sessionId);
}
// Otherwise just try to restart from the cookie
RootAuthenticationSessionModel existingRootAuthSession = authSessionManager.getCurrentRootAuthenticationSession(realm);
response = restartAuthenticationSessionFromCookie(existingRootAuthSession);
return null;
}
@ -433,5 +430,4 @@ public class SessionCodeChecks {
return new AuthenticationFlowURLHelper(session, realm, uriInfo)
.showPageExpired(authSession);
}
}

View file

@ -17,19 +17,20 @@
package org.keycloak.services.util;
import java.net.URI;
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.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.common.util.ServerCookie;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.services.managers.AuthenticationManager;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -37,6 +38,8 @@ import javax.ws.rs.core.UriInfo;
*/
public class CookieHelper {
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
*
@ -58,10 +61,36 @@ public class CookieHelper {
}
public static String getCookieValue(String name) {
public static Set<String> getCookieValue(String name) {
HttpHeaders headers = ResteasyProviderFactory.getContextData(HttpHeaders.class);
Cookie cookie = headers.getCookies().get(name);
return cookie != null ? cookie.getValue() : null;
Set<String> cookiesVal = new HashSet<>();
// check for cookies in the request headers
List<String> cookieHeader = headers.getRequestHeaders().get(HttpHeaders.COOKIE);
if (cookieHeader != null) {
logger.debugv("{1} cookie found in the request's header", name);
cookieHeader.stream().map(s -> parseCookie(s, name)).forEach(cookiesVal::addAll);
}
// get cookies from the cookie field
Cookie cookie = headers.getCookies().get(name);
if (cookie != null) {
logger.debugv("{1} cookie found in the cookie's field", name);
cookiesVal.add(cookie.getValue());
}
return cookiesVal;
}
public static Set<String> parseCookie(String cookieHeader, String name) {
String parts[] = cookieHeader.split("[;,]");
Set<String> cookies = Arrays.stream(parts).filter(part -> part.startsWith(name + "=")).map(part ->
part.substring(part.indexOf('=') + 1)).collect(Collectors.toSet());
return cookies;
}
}

View file

@ -0,0 +1,292 @@
package org.keycloak.testsuite.cookies;
import org.apache.commons.io.IOUtils;
import org.apache.http.NameValuePair;
import org.apache.http.client.CookieStore;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
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.impl.client.LaxRedirectStrategy;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpCoreContext;
import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.models.AccountRoles;
import org.keycloak.models.AdminRoles;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.ActionURIUtils;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.URLUtils;
import org.keycloak.testsuite.util.UserBuilder;
import org.openqa.selenium.Cookie;
import java.io.IOException;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class CookiesPathTest extends AbstractKeycloakTest {
@Page
protected LoginPage loginPage;
public static final String AUTH_SESSION_VALUE = "1869c345-2f90-4724-936d-a1a1ef41dea7";
public static final String AUTH_SESSION_VALUE_NODE = "1869c345-2f90-4724-936d-a1a1ef41dea7.host";
public static final String OLD_COOKIE_PATH = "/auth/realms/foo";
public static final String KC_RESTART = "KC_RESTART";
@Test
public void testCookiesPath() {
// navigate to "/realms/foo/account" and remove cookies in the browser for the current path
// first access to the path means there are no cookies being sent
// we are redirected to login page and Keycloak sets cookie's path to "/auth/realms/foo/"
deleteAllCookiesForRealm("foo");
Assert.assertTrue("There shouldn't be any cookies sent!", driver.manage().getCookies().isEmpty());
// refresh the page and cookies are sent within the request
driver.navigate().refresh();
Set<Cookie> cookies = driver.manage().getCookies();
Assert.assertTrue("There should be cookies sent!", cookies.size() > 0);
// check cookie's path, for some reason IE adds extra slash to the beginning of the path
cookies.stream().forEach(cookie -> Assert.assertThat(cookie.getPath(), Matchers.endsWith("/auth/realms/foo/")));
// now navigate to realm which name overlaps the first realm and delete cookies for that realm (foobar)
deleteAllCookiesForRealm("foobar");
// cookies shouldn't be sent for the first access to /realms/foobar/account
// At this moment IE would sent cookies for /auth/realms/foo without the fix
cookies = driver.manage().getCookies();
Assert.assertTrue("There shouldn't be any cookies sent!", cookies.isEmpty());
// refresh the page and check if correct cookies were sent
driver.navigate().refresh();
cookies = driver.manage().getCookies();
Assert.assertTrue("There should be cookies sent!", cookies.size() > 0);
// check cookie's path, for some reason IE adds extra slash to the beginning of the path
cookies.stream().forEach(cookie -> Assert.assertThat(cookie.getPath(), Matchers.endsWith("/auth/realms/foobar/")));
// lets back to "/realms/foo/account" to test the cookies for "foo" realm are still there and haven't been (correctly) sent to "foobar"
URLUtils.navigateToUri( oauth.AUTH_SERVER_ROOT + "/realms/foo/account", true);
cookies = driver.manage().getCookies();
Assert.assertTrue("There should be cookies sent!", cookies.size() > 0);
cookies.stream().forEach(cookie -> Assert.assertThat(cookie.getPath(), Matchers.endsWith("/auth/realms/foo/")));
}
@Test
public void testMultipleCookies() throws IOException {
String requestURI = oauth.AUTH_SERVER_ROOT + "/realms/foo/account";
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_YEAR, 1);
// create old cookie with wrong path
BasicClientCookie wrongCookie = new BasicClientCookie(AuthenticationSessionManager.AUTH_SESSION_ID, AUTH_SESSION_VALUE);
wrongCookie.setDomain("localhost");
wrongCookie.setPath(OLD_COOKIE_PATH);
wrongCookie.setExpiryDate(calendar.getTime());
// obtain new cookies
CookieStore cookieStore = getCorrectCookies(requestURI);
cookieStore.addCookie(wrongCookie);
Assert.assertThat(cookieStore.getCookies(), Matchers.hasSize(3));
CloseableHttpResponse response = login(requestURI, cookieStore);
response.close();
// old cookie has been removed
// now we have AUTH_SESSION_ID, KEYCLOAK_IDENTITY, KEYCLOAK_SESSION, OAuth_Token_Request_State
Assert.assertThat(cookieStore.getCookies(), Matchers.hasSize(4));
// does each cookie's path end with "/"
cookieStore.getCookies().stream().filter(c -> !"OAuth_Token_Request_State".equals(c.getName()))
.map(c -> c.getPath()).forEach(path ->Assert.assertThat(path, Matchers.endsWith("/")));
// KEYCLOAK_SESSION should end by AUTH_SESSION_ID value
String authSessionId = cookieStore.getCookies().stream().filter(c -> "AUTH_SESSION_ID".equals(c.getName())).findFirst().get().getValue();
String KCSessionId = cookieStore.getCookies().stream().filter(c -> "KEYCLOAK_SESSION".equals(c.getName())).findFirst().get().getValue();
String KCSessionSuffix = KCSessionId.split("/")[2];
Assert.assertThat(authSessionId, Matchers.containsString(KCSessionSuffix));
}
@Test
public void testOldCookieWithWrongPath() {
Cookie wrongCookie = new Cookie(AuthenticationSessionManager.AUTH_SESSION_ID, AUTH_SESSION_VALUE,
null, OLD_COOKIE_PATH, null, false, true);
deleteAllCookiesForRealm("foo");
// add old cookie with wrong path
driver.manage().addCookie(wrongCookie);
Set<Cookie> cookies = driver.manage().getCookies();
Assert.assertThat(cookies, Matchers.hasSize(1));
oauth.realm("foo").redirectUri(OAuthClient.AUTH_SERVER_ROOT + "/realms/foo/account").clientId("account").openLoginForm();
loginPage.login("foo", "password");
// old cookie has been removed and new cookies have been added
cookies = driver.manage().getCookies();
Assert.assertThat(cookies, Matchers.hasSize(3));
// does each cookie's path end with "/"
cookies.stream().map(c -> c.getPath()).forEach(path ->
Assert.assertThat(path, Matchers.endsWith("/")));
// KEYCLOAK_SESSION should end by AUTH_SESSION_ID value
String authSessionId = cookies.stream().filter(c -> "AUTH_SESSION_ID".equals(c.getName())).findFirst().get().getValue();
String KCSessionId = cookies.stream().filter(c -> "KEYCLOAK_SESSION".equals(c.getName())).findFirst().get().getValue();
String KCSessionSuffix = KCSessionId.split("/")[2];
Assert.assertThat(authSessionId, Matchers.containsString(KCSessionSuffix));
}
@Test
public void testOldCookieWithNodeInValue() throws IOException {
String requestURI = oauth.AUTH_SERVER_ROOT + "/realms/foo/account";
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_YEAR, 1);
// create old cookie with wrong path
BasicClientCookie wrongCookie = new BasicClientCookie(AuthenticationSessionManager.AUTH_SESSION_ID, AUTH_SESSION_VALUE_NODE);
wrongCookie.setDomain("localhost");
wrongCookie.setPath(OLD_COOKIE_PATH);
wrongCookie.setExpiryDate(calendar.getTime());
// obtain new cookies
CookieStore cookieStore = getCorrectCookies(requestURI);
cookieStore.addCookie(wrongCookie);
Assert.assertThat(cookieStore.getCookies(), Matchers.hasSize(3));
CloseableHttpResponse response = login(requestURI, cookieStore);
response.close();
// old cookie has been removed
// now we have AUTH_SESSION_ID, KEYCLOAK_IDENTITY, KEYCLOAK_SESSION, OAuth_Token_Request_State
Assert.assertThat(cookieStore.getCookies(), Matchers.hasSize(4));
// does each cookie's path end with "/"
cookieStore.getCookies().stream().filter(c -> !"OAuth_Token_Request_State".equals(c.getName()))
.map(c -> c.getPath()).forEach(path ->Assert.assertThat(path, Matchers.endsWith("/")));
// KEYCLOAK_SESSION should end by AUTH_SESSION_ID value
String authSessionId = cookieStore.getCookies().stream().filter(c -> "AUTH_SESSION_ID".equals(c.getName())).findFirst().get().getValue();
String KCSessionId = cookieStore.getCookies().stream().filter(c -> "KEYCLOAK_SESSION".equals(c.getName())).findFirst().get().getValue();
String KCSessionSuffix = KCSessionId.split("/")[2];
Assert.assertThat(authSessionId, Matchers.containsString(KCSessionSuffix));
}
/**
* Add two realms which names are overlapping i.e foo and foobar
* @param testRealms
*/
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmBuilder foo = RealmBuilder.create().name("foo");
foo.user(UserBuilder.create().username("foo").password("password").role("account", AdminRoles.ADMIN)
.role("account", AccountRoles.MANAGE_ACCOUNT).role("account", AccountRoles.VIEW_PROFILE).role("account", AccountRoles.MANAGE_ACCOUNT_LINKS));
testRealms.add(foo.build());
RealmBuilder foobar = RealmBuilder.create().name("foobar");
foo.user(UserBuilder.create().username("foobar").password("password").role("account", AdminRoles.ADMIN)
.role("account", AccountRoles.MANAGE_ACCOUNT).role("account", AccountRoles.VIEW_PROFILE).role("account", AccountRoles.MANAGE_ACCOUNT_LINKS));
testRealms.add(foobar.build());
}
private CloseableHttpResponse sendRequest(HttpRequestBase request, CookieStore cookieStore, HttpCoreContext localContext) throws IOException {
CloseableHttpClient c = HttpClientBuilder.create().setDefaultCookieStore(cookieStore).setRedirectStrategy(new LaxRedirectStrategy()).build();
CloseableHttpResponse response = c.execute(request, localContext);
return response;
}
private CookieStore getCorrectCookies(String uri) throws IOException {
CookieStore cookieStore = new BasicCookieStore();
HttpGet request = new HttpGet(uri);
CloseableHttpResponse response = sendRequest(request, new BasicCookieStore(), new HttpCoreContext());
for (org.apache.http.Header h: response.getHeaders("Set-Cookie")) {
if (h.getValue().contains(AuthenticationSessionManager.AUTH_SESSION_ID)) {
cookieStore.addCookie(parseCookie(h.getValue(), AuthenticationSessionManager.AUTH_SESSION_ID));
} else if (h.getValue().contains(KC_RESTART)) {
cookieStore.addCookie(parseCookie(h.getValue(), KC_RESTART));
}
}
response.close();
return cookieStore;
}
private BasicClientCookie parseCookie(String line, String name) {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_YEAR, 1);
String path = "";
String value = "";
for (String s: line.split(";")) {
if (s.contains(name)) {
String[] split = s.split("=");
value = split[1];
} else if (s.contains("Path")) {
String[] split = s.split("=");
path = split[1];
}
}
BasicClientCookie c = new BasicClientCookie(name, value);
c.setExpiryDate(calendar.getTime());
c.setDomain("localhost");
c.setPath(path);
return c;
}
private CloseableHttpResponse login(String requestURI, CookieStore cookieStore) throws IOException {
HttpCoreContext httpContext = new HttpCoreContext();
HttpGet request = new HttpGet(requestURI);
// send an initial request, we are redirected to login page
CloseableHttpResponse response = sendRequest(request, cookieStore, httpContext);
String s = IOUtils.toString(response.getEntity().getContent(), "UTF-8");
response.close();
String action = ActionURIUtils.getActionURIFromPageSource(s);
// send credentials to login form
HttpPost post = new HttpPost(action);
List<NameValuePair> params = new LinkedList<>();
params.add(new BasicNameValuePair("username", "foo"));
params.add(new BasicNameValuePair("password", "password"));
post.setHeader("Content-Type", "application/x-www-form-urlencoded");
post.setEntity(new UrlEncodedFormEntity(params));
return sendRequest(post, cookieStore, httpContext);
}
}