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
|
||||
RootAuthenticationSessionEntity entity = tx.get(cache, authSessionId);
|
||||
|
||||
|
@ -181,7 +181,7 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
|
|||
|
||||
@Override
|
||||
public RootAuthenticationSessionModel getRootAuthenticationSession(RealmModel realm, String authenticationSessionId) {
|
||||
RootAuthenticationSessionEntity entity = getRootAuthenticationSessionEntity(realm, authenticationSessionId);
|
||||
RootAuthenticationSessionEntity entity = getRootAuthenticationSessionEntity(authenticationSessionId);
|
||||
return wrap(realm, entity);
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.sessions;
|
|||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
|
|
|
@ -35,12 +35,9 @@ import org.keycloak.protocol.LoginProtocol.Error;
|
|||
import org.keycloak.services.ErrorPageException;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.services.managers.UserSessionCrossDCManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
import org.keycloak.services.util.CacheControlUtil;
|
||||
import org.keycloak.services.util.AuthenticationFlowURLHelper;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||
|
||||
|
@ -170,26 +167,25 @@ public abstract class AuthorizationEndpointBase {
|
|||
|
||||
protected AuthenticationSessionModel createAuthenticationSession(ClientModel client, String requestState) {
|
||||
AuthenticationSessionManager manager = new AuthenticationSessionManager(session);
|
||||
String authSessionId = manager.getCurrentAuthenticationSessionId(realm);
|
||||
RootAuthenticationSessionModel rootAuthSession = authSessionId==null ? null : session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
||||
RootAuthenticationSessionModel rootAuthSession = manager.getCurrentRootAuthenticationSession(realm);
|
||||
|
||||
AuthenticationSessionModel authSession;
|
||||
|
||||
if (rootAuthSession != null) {
|
||||
|
||||
authSession = rootAuthSession.createAuthenticationSession(client);
|
||||
|
||||
logger.debugf("Sent request to authz endpoint. Root authentication session with ID '%s' exists. Client is '%s' . Created new authentication session with tab ID: %s",
|
||||
rootAuthSession.getId(), client.getClientId(), authSession.getTabId());
|
||||
|
||||
} else {
|
||||
|
||||
UserSessionModel userSession = authSessionId == null ? null : new UserSessionCrossDCManager(session).getUserSessionIfExistsRemotely(realm, authSessionId);
|
||||
UserSessionCrossDCManager userSessionCrossDCManager = new UserSessionCrossDCManager(session);
|
||||
UserSessionModel userSession = userSessionCrossDCManager.getUserSessionIfExistsRemotely(manager, realm);
|
||||
|
||||
if (userSession != null) {
|
||||
rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(authSessionId, realm);
|
||||
String userSessionId = userSession.getId();
|
||||
rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(userSessionId, realm);
|
||||
authSession = rootAuthSession.createAuthenticationSession(client);
|
||||
logger.debugf("Sent request to authz endpoint. We don't have root authentication session with ID '%s' but we have userSession." +
|
||||
"Re-created root authentication session with same ID. Client is: %s . New authentication session tab ID: %s", authSessionId, client.getClientId(), authSession.getTabId());
|
||||
"Re-created root authentication session with same ID. Client is: %s . New authentication session tab ID: %s", userSessionId, client.getClientId(), authSession.getTabId());
|
||||
} else {
|
||||
rootAuthSession = manager.createAuthenticationSession(realm, true);
|
||||
authSession = rootAuthSession.createAuthenticationSession(client);
|
||||
|
|
|
@ -67,6 +67,7 @@ import java.net.URI;
|
|||
import java.security.PublicKey;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.AbstractMap.SimpleEntry;
|
||||
|
||||
/**
|
||||
* Stateless object that manages authentication
|
||||
|
@ -223,20 +224,22 @@ public class AuthenticationManager {
|
|||
// Account management client is used as a placeholder
|
||||
ClientModel client = SystemClientUtil.getSystemClient(realm);
|
||||
|
||||
// Try to lookup current authSessionId from browser cookie. If doesn't exists, use the same as current userSession
|
||||
String authSessionId = null;
|
||||
String authSessionId;
|
||||
RootAuthenticationSessionModel rootLogoutSession = null;
|
||||
boolean browserCookiePresent = false;
|
||||
|
||||
// Try to lookup current authSessionId from browser cookie. If doesn't exists, use the same as current userSession
|
||||
if (browserCookie) {
|
||||
authSessionId = asm.getCurrentAuthenticationSessionId(realm);
|
||||
rootLogoutSession = asm.getCurrentRootAuthenticationSession(realm);
|
||||
}
|
||||
if (authSessionId != null) {
|
||||
if (rootLogoutSession != null) {
|
||||
authSessionId = rootLogoutSession.getId();
|
||||
browserCookiePresent = true;
|
||||
} else {
|
||||
authSessionId = userSession.getId();
|
||||
rootLogoutSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
||||
}
|
||||
|
||||
// Try to join existing logout session if it exists
|
||||
RootAuthenticationSessionModel rootLogoutSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
||||
if (rootLogoutSession == null) {
|
||||
rootLogoutSession = session.authenticationSessions().createRootAuthenticationSession(authSessionId, realm);
|
||||
}
|
||||
|
@ -615,7 +618,20 @@ public class AuthenticationManager {
|
|||
String path = getIdentityCookiePath(realm, uriInfo);
|
||||
expireCookie(realm, KEYCLOAK_IDENTITY_COOKIE, path, true, connection);
|
||||
expireCookie(realm, KEYCLOAK_SESSION_COOKIE, path, false, connection);
|
||||
|
||||
String oldPath = getOldCookiePath(realm, uriInfo);
|
||||
expireCookie(realm, KEYCLOAK_IDENTITY_COOKIE, oldPath, true, connection);
|
||||
expireCookie(realm, KEYCLOAK_SESSION_COOKIE, oldPath, false, connection);
|
||||
}
|
||||
public static void expireOldIdentityCookie(RealmModel realm, UriInfo uriInfo, ClientConnection connection) {
|
||||
logger.debug("Expiring old identity cookie with wrong path");
|
||||
|
||||
String oldPath = getOldCookiePath(realm, uriInfo);
|
||||
expireCookie(realm, KEYCLOAK_IDENTITY_COOKIE, oldPath, true, connection);
|
||||
expireCookie(realm, KEYCLOAK_SESSION_COOKIE, oldPath, false, connection);
|
||||
}
|
||||
|
||||
|
||||
public static void expireRememberMeCookie(RealmModel realm, UriInfo uriInfo, ClientConnection connection) {
|
||||
logger.debug("Expiring remember me cookie");
|
||||
String path = getIdentityCookiePath(realm, uriInfo);
|
||||
|
@ -623,11 +639,24 @@ public class AuthenticationManager {
|
|||
expireCookie(realm, cookieName, path, true, connection);
|
||||
}
|
||||
|
||||
public static void expireOldAuthSessionCookie(RealmModel realm, UriInfo uriInfo, ClientConnection connection) {
|
||||
logger.debugv("Expire {1} cookie .", AuthenticationSessionManager.AUTH_SESSION_ID);
|
||||
|
||||
String oldPath = getOldCookiePath(realm, uriInfo);
|
||||
expireCookie(realm, AuthenticationSessionManager.AUTH_SESSION_ID, oldPath, true, connection);
|
||||
}
|
||||
|
||||
protected static String getIdentityCookiePath(RealmModel realm, UriInfo uriInfo) {
|
||||
return getRealmCookiePath(realm, uriInfo);
|
||||
}
|
||||
|
||||
public static String getRealmCookiePath(RealmModel realm, UriInfo uriInfo) {
|
||||
URI uri = RealmsResource.realmBaseUrl(uriInfo).build(realm.getName());
|
||||
// KEYCLOAK-5270
|
||||
return uri.getRawPath() + "/";
|
||||
}
|
||||
|
||||
public static String getOldCookiePath(RealmModel realm, UriInfo uriInfo) {
|
||||
URI uri = RealmsResource.realmBaseUrl(uriInfo).build(realm.getName());
|
||||
return uri.getRawPath();
|
||||
}
|
||||
|
@ -658,6 +687,7 @@ public class AuthenticationManager {
|
|||
AuthResult authResult = verifyIdentityToken(session, realm, session.getContext().getUri(), session.getContext().getConnection(), checkActive, false, true, tokenString, session.getContext().getRequestHeaders());
|
||||
if (authResult == null) {
|
||||
expireIdentityCookie(realm, session.getContext().getUri(), session.getContext().getConnection());
|
||||
expireOldIdentityCookie(realm, session.getContext().getUri(), session.getContext().getConnection());
|
||||
return null;
|
||||
}
|
||||
authResult.getSession().setLastSessionRefresh(Time.currentTime());
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
|
||||
package org.keycloak.services.managers;
|
||||
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.models.ClientModel;
|
||||
|
@ -31,6 +29,13 @@ import org.keycloak.sessions.AuthenticationSessionModel;
|
|||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||
import org.keycloak.sessions.StickySessionEncoderProvider;
|
||||
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.util.AbstractMap.SimpleEntry;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
|
@ -38,6 +43,8 @@ public class AuthenticationSessionManager {
|
|||
|
||||
public static final String AUTH_SESSION_ID = "AUTH_SESSION_ID";
|
||||
|
||||
public static final int AUTH_SESSION_LIMIT = 3;
|
||||
|
||||
private static final Logger log = Logger.getLogger(AuthenticationSessionManager.class);
|
||||
|
||||
private final KeycloakSession session;
|
||||
|
@ -64,14 +71,40 @@ public class AuthenticationSessionManager {
|
|||
return rootAuthSession;
|
||||
}
|
||||
|
||||
public RootAuthenticationSessionModel getCurrentRootAuthenticationSession(RealmModel realm) {
|
||||
List<String> authSessionIds = getAuthSessionCookieIds(realm);
|
||||
|
||||
/**
|
||||
* Returns ID of current authentication session if it exists, otherwise returns {@code null}.
|
||||
* @param realm
|
||||
* @return
|
||||
*/
|
||||
public String getCurrentAuthenticationSessionId(RealmModel realm) {
|
||||
return getAuthSessionCookieDecoded(realm);
|
||||
return authSessionIds.stream().map(id -> {
|
||||
SimpleEntry<String, String> entry = decodeAuthSessionId(id);
|
||||
String sessionId = entry.getKey();
|
||||
|
||||
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, sessionId);
|
||||
|
||||
if (rootAuthSession != null) {
|
||||
reencodeAuthSessionCookie(sessionId, entry.getValue(), realm);
|
||||
return rootAuthSession;
|
||||
}
|
||||
|
||||
return null;
|
||||
}).filter(authSession -> Objects.nonNull(authSession)).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public UserSessionModel getUserSessionFromAuthCookie(RealmModel realm) {
|
||||
List<String> authSessionIds = getAuthSessionCookieIds(realm);
|
||||
|
||||
return authSessionIds.stream().map(id -> {
|
||||
SimpleEntry<String, String> entry = decodeAuthSessionId(id);
|
||||
String sessionId = entry.getKey();
|
||||
|
||||
UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
|
||||
|
||||
if (userSession != null) {
|
||||
reencodeAuthSessionCookie(sessionId, entry.getValue(), realm);
|
||||
return userSession;
|
||||
}
|
||||
|
||||
return null;
|
||||
}).filter(authSession -> Objects.nonNull(authSession)).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
|
||||
|
@ -81,13 +114,21 @@ public class AuthenticationSessionManager {
|
|||
* @return
|
||||
*/
|
||||
public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm, ClientModel client, String tabId) {
|
||||
String authSessionId = getAuthSessionCookieDecoded(realm);
|
||||
List<String> authSessionIds = getAuthSessionCookieIds(realm);
|
||||
|
||||
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 getAuthenticationSessionByIdAndClient(realm, authSessionId, client, tabId);
|
||||
}).filter(authSession -> Objects.nonNull(authSession)).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
|
||||
|
@ -105,30 +146,38 @@ public class AuthenticationSessionManager {
|
|||
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) {
|
||||
String cookieVal = CookieHelper.getCookieValue(AUTH_SESSION_ID);
|
||||
return new SimpleEntry(decodedAuthSessionId, reencoded);
|
||||
}
|
||||
|
||||
if (cookieVal != null) {
|
||||
log.debugf("Found AUTH_SESSION_ID cookie with value %s", cookieVal);
|
||||
|
||||
StickySessionEncoderProvider encoder = session.getProvider(StickySessionEncoderProvider.class);
|
||||
String decodedAuthSessionId = encoder.decodeSessionId(cookieVal);
|
||||
|
||||
// Check if owner of this authentication session changed due to re-hashing (usually node failover or addition of new node)
|
||||
String 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 void reencodeAuthSessionCookie(String decodedAuthSessionId, String reencodedAuthSessionId, RealmModel realm) {
|
||||
if (!decodedAuthSessionId.equals(reencodedAuthSessionId)) {
|
||||
log.debugf("Route changed. Will update authentication session cookie");
|
||||
setAuthSessionCookie(decodedAuthSessionId, realm);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
RootAuthenticationSessionModel rootAuthSession = authSession.getParentSession();
|
||||
|
@ -156,5 +205,4 @@ public class AuthenticationSessionManager {
|
|||
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
||||
return rootAuthSession==null ? null : rootAuthSession.getAuthenticationSession(client, tabId);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,14 +17,15 @@
|
|||
|
||||
package org.keycloak.services.managers;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.AbstractMap.SimpleEntry;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.sessions.CommonClientSessionModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -60,14 +61,24 @@ public class UserSessionCrossDCManager {
|
|||
|
||||
|
||||
// Just check if userSession also exists on remoteCache. It can happen that logout happened on 2nd DC and userSession is already removed on remoteCache and this DC wasn't yet notified
|
||||
public UserSessionModel getUserSessionIfExistsRemotely(RealmModel realm, String id) {
|
||||
UserSessionModel userSession = kcSession.sessions().getUserSession(realm, id);
|
||||
public UserSessionModel getUserSessionIfExistsRemotely(AuthenticationSessionManager asm, RealmModel realm) {
|
||||
List<String> sessionIds = asm.getAuthSessionCookieIds(realm);
|
||||
|
||||
// This will remove userSession "locally" if it doesn't exists on remoteCache
|
||||
kcSession.sessions().getUserSessionWithPredicate(realm, id, false, (UserSessionModel userSession2) -> {
|
||||
return userSession2 == null;
|
||||
});
|
||||
return sessionIds.stream().map(id -> {
|
||||
SimpleEntry<String, String> entry = asm.decodeAuthSessionId(id);
|
||||
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.KEY) String key) {
|
||||
if (key != null) {
|
||||
return handleActionToken(authSessionId, key, execution, clientId, tabId);
|
||||
return handleActionToken(key, execution, clientId, tabId);
|
||||
}
|
||||
|
||||
event.event(EventType.RESET_PASSWORD);
|
||||
|
@ -422,10 +422,10 @@ public class LoginActionsService {
|
|||
@QueryParam("execution") String execution,
|
||||
@QueryParam("client_id") String clientId,
|
||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||
return handleActionToken(authSessionId, key, execution, clientId, tabId);
|
||||
return handleActionToken(key, execution, clientId, tabId);
|
||||
}
|
||||
|
||||
protected <T extends JsonWebToken & ActionTokenKeyModel> Response handleActionToken(String authSessionId, String tokenString, String execution, String clientId, String tabId) {
|
||||
protected <T extends JsonWebToken & ActionTokenKeyModel> Response handleActionToken(String tokenString, String execution, String clientId, String tabId) {
|
||||
T token;
|
||||
ActionTokenHandler<T> handler;
|
||||
ActionTokenContext<T> tokenContext;
|
||||
|
@ -442,7 +442,6 @@ public class LoginActionsService {
|
|||
AuthenticationSessionManager authenticationSessionManager = new AuthenticationSessionManager(session);
|
||||
if (client != null) {
|
||||
session.getContext().setClient(client);
|
||||
authSessionId = authSessionId == null ? authenticationSessionManager.getAuthSessionCookieDecoded(realm) : authSessionId;
|
||||
authSession = authenticationSessionManager.getCurrentAuthenticationSession(realm, client, tabId);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,10 @@
|
|||
package org.keycloak.services.resources;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.AbstractMap.SimpleEntry;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
@ -73,7 +77,6 @@ public class SessionCodeChecks {
|
|||
private final String flowPath;
|
||||
private final String authSessionId;
|
||||
|
||||
|
||||
public SessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event,
|
||||
String authSessionId, String code, String execution, String clientId, String tabId, String flowPath) {
|
||||
this.realm = realm;
|
||||
|
@ -175,28 +178,22 @@ public class SessionCodeChecks {
|
|||
}
|
||||
|
||||
// See if we are already authenticated and userSession with same ID exists.
|
||||
String sessionId = authSessionManager.getCurrentAuthenticationSessionId(realm);
|
||||
RootAuthenticationSessionModel existingRootAuthSession = null;
|
||||
if (sessionId != null) {
|
||||
UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
|
||||
if (userSession != null) {
|
||||
UserSessionModel userSession = authSessionManager.getUserSessionFromAuthCookie(realm);
|
||||
|
||||
LoginFormsProvider loginForm = session.getProvider(LoginFormsProvider.class).setAuthenticationSession(authSession)
|
||||
.setSuccess(Messages.ALREADY_LOGGED_IN);
|
||||
if (userSession != null) {
|
||||
LoginFormsProvider loginForm = session.getProvider(LoginFormsProvider.class).setAuthenticationSession(authSession)
|
||||
.setSuccess(Messages.ALREADY_LOGGED_IN);
|
||||
|
||||
if (client == null) {
|
||||
loginForm.setAttribute(Constants.SKIP_LINK, true);
|
||||
}
|
||||
|
||||
response = loginForm.createInfoPage();
|
||||
return null;
|
||||
if (client == null) {
|
||||
loginForm.setAttribute(Constants.SKIP_LINK, true);
|
||||
}
|
||||
|
||||
|
||||
existingRootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, sessionId);
|
||||
response = loginForm.createInfoPage();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Otherwise just try to restart from the cookie
|
||||
RootAuthenticationSessionModel existingRootAuthSession = authSessionManager.getCurrentRootAuthenticationSession(realm);
|
||||
response = restartAuthenticationSessionFromCookie(existingRootAuthSession);
|
||||
return null;
|
||||
}
|
||||
|
@ -433,5 +430,4 @@ public class SessionCodeChecks {
|
|||
return new AuthenticationFlowURLHelper(session, realm, uriInfo)
|
||||
.showPageExpired(authSession);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,19 +17,20 @@
|
|||
|
||||
package org.keycloak.services.util;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.spi.HttpResponse;
|
||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
import org.keycloak.common.util.ServerCookie;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
|
||||
import javax.ws.rs.core.Cookie;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -37,6 +38,8 @@ import javax.ws.rs.core.UriInfo;
|
|||
*/
|
||||
public class CookieHelper {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(CookieHelper.class);
|
||||
|
||||
/**
|
||||
* Set a response cookie. This solely exists because JAX-RS 1.1 does not support setting HttpOnly cookies
|
||||
*
|
||||
|
@ -58,10 +61,36 @@ public class CookieHelper {
|
|||
}
|
||||
|
||||
|
||||
public static String getCookieValue(String name) {
|
||||
public static Set<String> getCookieValue(String name) {
|
||||
HttpHeaders headers = ResteasyProviderFactory.getContextData(HttpHeaders.class);
|
||||
|
||||
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);
|
||||
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