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

View file

@ -20,6 +20,7 @@ package org.keycloak.sessions;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
import java.util.Map; 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.ErrorPageException;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.UserSessionCrossDCManager; import org.keycloak.services.managers.UserSessionCrossDCManager;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService; 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.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel; import org.keycloak.sessions.RootAuthenticationSessionModel;
@ -170,26 +167,25 @@ public abstract class AuthorizationEndpointBase {
protected AuthenticationSessionModel createAuthenticationSession(ClientModel client, String requestState) { protected AuthenticationSessionModel createAuthenticationSession(ClientModel client, String requestState) {
AuthenticationSessionManager manager = new AuthenticationSessionManager(session); AuthenticationSessionManager manager = new AuthenticationSessionManager(session);
String authSessionId = manager.getCurrentAuthenticationSessionId(realm); RootAuthenticationSessionModel rootAuthSession = manager.getCurrentRootAuthenticationSession(realm);
RootAuthenticationSessionModel rootAuthSession = authSessionId==null ? null : session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
AuthenticationSessionModel authSession; AuthenticationSessionModel authSession;
if (rootAuthSession != null) { if (rootAuthSession != null) {
authSession = rootAuthSession.createAuthenticationSession(client); 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", 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()); rootAuthSession.getId(), client.getClientId(), authSession.getTabId());
} else { } else {
UserSessionCrossDCManager userSessionCrossDCManager = new UserSessionCrossDCManager(session);
UserSessionModel userSession = authSessionId == null ? null : new UserSessionCrossDCManager(session).getUserSessionIfExistsRemotely(realm, authSessionId); UserSessionModel userSession = userSessionCrossDCManager.getUserSessionIfExistsRemotely(manager, realm);
if (userSession != null) { if (userSession != null) {
rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(authSessionId, realm); String userSessionId = userSession.getId();
rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(userSessionId, realm);
authSession = rootAuthSession.createAuthenticationSession(client); 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." + 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 { } else {
rootAuthSession = manager.createAuthenticationSession(realm, true); rootAuthSession = manager.createAuthenticationSession(realm, true);
authSession = rootAuthSession.createAuthenticationSession(client); authSession = rootAuthSession.createAuthenticationSession(client);

View file

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

View file

@ -17,8 +17,6 @@
package org.keycloak.services.managers; package org.keycloak.services.managers;
import javax.ws.rs.core.UriInfo;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
@ -31,6 +29,13 @@ import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel; import org.keycloak.sessions.RootAuthenticationSessionModel;
import org.keycloak.sessions.StickySessionEncoderProvider; 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> * @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 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 static final Logger log = Logger.getLogger(AuthenticationSessionManager.class);
private final KeycloakSession session; private final KeycloakSession session;
@ -64,14 +71,40 @@ public class AuthenticationSessionManager {
return rootAuthSession; return rootAuthSession;
} }
public RootAuthenticationSessionModel getCurrentRootAuthenticationSession(RealmModel realm) {
List<String> authSessionIds = getAuthSessionCookieIds(realm);
/** return authSessionIds.stream().map(id -> {
* Returns ID of current authentication session if it exists, otherwise returns {@code null}. SimpleEntry<String, String> entry = decodeAuthSessionId(id);
* @param realm String sessionId = entry.getKey();
* @return
*/ RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, sessionId);
public String getCurrentAuthenticationSessionId(RealmModel realm) {
return getAuthSessionCookieDecoded(realm); 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 * @return
*/ */
public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm, ClientModel client, String tabId) { public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm, ClientModel client, String tabId) {
String authSessionId = getAuthSessionCookieDecoded(realm); List<String> authSessionIds = getAuthSessionCookieIds(realm);
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;
}
if (authSessionId == null) {
return null; return null;
} }).filter(authSession -> Objects.nonNull(authSession)).findFirst().orElse(null);
return getAuthenticationSessionByIdAndClient(realm, authSessionId, client, tabId);
} }
@ -105,30 +146,38 @@ public class AuthenticationSessionManager {
log.debugf("Set AUTH_SESSION_ID cookie with value %s", encodedAuthSessionId); log.debugf("Set AUTH_SESSION_ID cookie with value %s", encodedAuthSessionId);
} }
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(authSessionId);
String reencoded = encoder.encodeSessionId(decodedAuthSessionId);
public String getAuthSessionCookieDecoded(RealmModel realm) { return new SimpleEntry(decodedAuthSessionId, reencoded);
String cookieVal = CookieHelper.getCookieValue(AUTH_SESSION_ID); }
if (cookieVal != null) { public void reencodeAuthSessionCookie(String decodedAuthSessionId, String reencodedAuthSessionId, RealmModel realm) {
log.debugf("Found AUTH_SESSION_ID cookie with value %s", cookieVal); if (!decodedAuthSessionId.equals(reencodedAuthSessionId)) {
log.debugf("Route changed. Will update authentication session cookie");
StickySessionEncoderProvider encoder = session.getProvider(StickySessionEncoderProvider.class); setAuthSessionCookie(decodedAuthSessionId, realm);
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 reencoded = encoder.encodeSessionId(decodedAuthSessionId);
if (!reencoded.equals(cookieVal)) {
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;
}
public void removeAuthenticationSession(RealmModel realm, AuthenticationSessionModel authSession, boolean expireRestartCookie) { public void removeAuthenticationSession(RealmModel realm, AuthenticationSessionModel authSession, boolean expireRestartCookie) {
RootAuthenticationSessionModel rootAuthSession = authSession.getParentSession(); RootAuthenticationSessionModel rootAuthSession = authSession.getParentSession();
@ -156,5 +205,4 @@ public class AuthenticationSessionManager {
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId); RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
return rootAuthSession==null ? null : rootAuthSession.getAuthenticationSession(client, tabId); return rootAuthSession==null ? null : rootAuthSession.getAuthenticationSession(client, tabId);
} }
} }

View file

@ -17,14 +17,15 @@
package org.keycloak.services.managers; 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.jboss.logging.Logger;
import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.sessions.CommonClientSessionModel;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @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 // 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) { public UserSessionModel getUserSessionIfExistsRemotely(AuthenticationSessionManager asm, RealmModel realm) {
UserSessionModel userSession = kcSession.sessions().getUserSession(realm, id); List<String> sessionIds = asm.getAuthSessionCookieIds(realm);
// This will remove userSession "locally" if it doesn't exists on remoteCache return sessionIds.stream().map(id -> {
kcSession.sessions().getUserSessionWithPredicate(realm, id, false, (UserSessionModel userSession2) -> { SimpleEntry<String, String> entry = asm.decodeAuthSessionId(id);
return userSession2 == null; String sessionId = entry.getKey();
});
return kcSession.sessions().getUserSession(realm, id); // This will remove userSession "locally" if it doesn't exists on remoteCache
kcSession.sessions().getUserSessionWithPredicate(realm, sessionId, false, (UserSessionModel userSession2) -> userSession2 == null);
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.TAB_ID) String tabId,
@QueryParam(Constants.KEY) String key) { @QueryParam(Constants.KEY) String key) {
if (key != null) { if (key != null) {
return handleActionToken(authSessionId, key, execution, clientId, tabId); return handleActionToken(key, execution, clientId, tabId);
} }
event.event(EventType.RESET_PASSWORD); event.event(EventType.RESET_PASSWORD);
@ -422,10 +422,10 @@ public class LoginActionsService {
@QueryParam("execution") String execution, @QueryParam("execution") String execution,
@QueryParam("client_id") String clientId, @QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) { @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; T token;
ActionTokenHandler<T> handler; ActionTokenHandler<T> handler;
ActionTokenContext<T> tokenContext; ActionTokenContext<T> tokenContext;
@ -442,7 +442,6 @@ public class LoginActionsService {
AuthenticationSessionManager authenticationSessionManager = new AuthenticationSessionManager(session); AuthenticationSessionManager authenticationSessionManager = new AuthenticationSessionManager(session);
if (client != null) { if (client != null) {
session.getContext().setClient(client); session.getContext().setClient(client);
authSessionId = authSessionId == null ? authenticationSessionManager.getAuthSessionCookieDecoded(realm) : authSessionId;
authSession = authenticationSessionManager.getCurrentAuthenticationSession(realm, client, tabId); authSession = authenticationSessionManager.getCurrentAuthenticationSession(realm, client, tabId);
} }

View file

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

View file

@ -17,19 +17,20 @@
package org.keycloak.services.util; 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.HttpResponse;
import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.common.util.ServerCookie; 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.Cookie;
import javax.ws.rs.core.HttpHeaders; 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> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -37,6 +38,8 @@ import javax.ws.rs.core.UriInfo;
*/ */
public class CookieHelper { 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 * 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); HttpHeaders headers = ResteasyProviderFactory.getContextData(HttpHeaders.class);
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); Cookie cookie = headers.getCookies().get(name);
return cookie != null ? cookie.getValue() : null; 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);
}
}