KEYCLOAK-5270 Realm cookie path for IE<=11 users (#5106)
This commit is contained in:
parent
6339b62f8d
commit
f429469fc8
10 changed files with 493 additions and 91 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue