After tab1 finish authentication, make sure that rootAuthenticationSession is expired shortly
closes #23880
This commit is contained in:
parent
c7183049c8
commit
04777299b0
7 changed files with 80 additions and 15 deletions
|
@ -111,7 +111,7 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
|
||||||
if (tasks.containsKey(taskKey)) {
|
if (tasks.containsKey(taskKey)) {
|
||||||
throw new IllegalStateException("Can't add session: task in progress for session");
|
throw new IllegalStateException("Can't add session: task in progress for session");
|
||||||
} else {
|
} else {
|
||||||
tasks.put(taskKey, new CacheTaskWithValue<V>(value) {
|
tasks.put(taskKey, new CacheTaskWithValue<V>(value, lifespan, lifespanUnit) {
|
||||||
@Override
|
@Override
|
||||||
public void execute() {
|
public void execute() {
|
||||||
decorateCache(cache).put(key, value, lifespan, lifespanUnit);
|
decorateCache(cache).put(key, value, lifespan, lifespanUnit);
|
||||||
|
@ -160,16 +160,17 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
|
||||||
}
|
}
|
||||||
|
|
||||||
public <K, V> void replace(Cache<K, V> cache, K key, V value, long lifespan, TimeUnit lifespanUnit) {
|
public <K, V> void replace(Cache<K, V> cache, K key, V value, long lifespan, TimeUnit lifespanUnit) {
|
||||||
log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REPLACE, key);
|
log.tracev("Adding cache operation: {0} on {1}. Lifespan {2} {3}.", CacheOperation.REPLACE, key, lifespan, lifespanUnit);
|
||||||
|
|
||||||
Object taskKey = getTaskKey(cache, key);
|
Object taskKey = getTaskKey(cache, key);
|
||||||
CacheTask current = tasks.get(taskKey);
|
CacheTask current = tasks.get(taskKey);
|
||||||
if (current != null) {
|
if (current != null) {
|
||||||
if (current instanceof CacheTaskWithValue) {
|
if (current instanceof CacheTaskWithValue) {
|
||||||
((CacheTaskWithValue<V>) current).setValue(value);
|
((CacheTaskWithValue<V>) current).setValue(value);
|
||||||
|
((CacheTaskWithValue<V>) current).updateLifespan(lifespan, lifespanUnit);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tasks.put(taskKey, new CacheTaskWithValue<V>(value) {
|
tasks.put(taskKey, new CacheTaskWithValue<V>(value, lifespan, lifespanUnit) {
|
||||||
@Override
|
@Override
|
||||||
public void execute() {
|
public void execute() {
|
||||||
decorateCache(cache).replace(key, value, lifespan, lifespanUnit);
|
decorateCache(cache).replace(key, value, lifespan, lifespanUnit);
|
||||||
|
@ -256,9 +257,17 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
|
||||||
|
|
||||||
public static abstract class CacheTaskWithValue<V> implements CacheTask {
|
public static abstract class CacheTaskWithValue<V> implements CacheTask {
|
||||||
protected V value;
|
protected V value;
|
||||||
|
protected long lifespan;
|
||||||
|
protected TimeUnit lifespanUnit;
|
||||||
|
|
||||||
public CacheTaskWithValue(V value) {
|
public CacheTaskWithValue(V value) {
|
||||||
|
this(value, -1, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CacheTaskWithValue(V value, long lifespan, TimeUnit lifespanUnit) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
|
this.lifespan = lifespan;
|
||||||
|
this.lifespanUnit = lifespanUnit;
|
||||||
}
|
}
|
||||||
|
|
||||||
public V getValue() {
|
public V getValue() {
|
||||||
|
@ -269,6 +278,11 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateLifespan(long lifespan, TimeUnit lifespanUnit) {
|
||||||
|
this.lifespan = lifespan;
|
||||||
|
this.lifespanUnit = lifespanUnit;
|
||||||
|
}
|
||||||
|
|
||||||
public Operation getOperation() {
|
public Operation getOperation() {
|
||||||
return Operation.OTHER;
|
return Operation.OTHER;
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ public class RootAuthenticationSessionAdapter implements RootAuthenticationSessi
|
||||||
}
|
}
|
||||||
|
|
||||||
void update() {
|
void update() {
|
||||||
int expirationSeconds = SessionExpiration.getAuthSessionLifespan(realm);
|
int expirationSeconds = getTimestamp() - Time.currentTime() + SessionExpiration.getAuthSessionLifespan(realm);
|
||||||
provider.tx.replace(cache, entity.getId(), entity, expirationSeconds, TimeUnit.SECONDS);
|
provider.tx.replace(cache, entity.getId(), entity, expirationSeconds, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -67,16 +67,11 @@ public class AuthenticationStateCookie {
|
||||||
this.remainingTabs = remainingTabs;
|
this.remainingTabs = remainingTabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void generateAndSetCookie(KeycloakSession session, RealmModel realm, RootAuthenticationSessionModel rootAuthSession) {
|
public static void generateAndSetCookie(KeycloakSession session, RealmModel realm, RootAuthenticationSessionModel rootAuthSession, int cookieMaxAge) {
|
||||||
UriInfo uriInfo = session.getContext().getHttpRequest().getUri();
|
UriInfo uriInfo = session.getContext().getHttpRequest().getUri();
|
||||||
String path = AuthenticationManager.getRealmCookiePath(realm, uriInfo);
|
String path = AuthenticationManager.getRealmCookiePath(realm, uriInfo);
|
||||||
boolean secureOnly = realm.getSslRequired().isRequired(session.getContext().getConnection());
|
boolean secureOnly = realm.getSslRequired().isRequired(session.getContext().getConnection());
|
||||||
|
|
||||||
// 1 minute by default. Same timeout, which is used for client to complete "authorization code" flow
|
|
||||||
// Very short timeout should be OK as when this cookie is set, other existing browser tabs are supposed to be refreshed immediatelly by JS script
|
|
||||||
// and login user automatically. No need to have cookie living any further
|
|
||||||
int cookieMaxAge = realm.getAccessCodeLifespan();
|
|
||||||
|
|
||||||
AuthenticationStateCookie cookie = new AuthenticationStateCookie();
|
AuthenticationStateCookie cookie = new AuthenticationStateCookie();
|
||||||
cookie.setAuthSessionId(rootAuthSession.getId());
|
cookie.setAuthSessionId(rootAuthSession.getId());
|
||||||
cookie.setRemainingTabs(rootAuthSession.getAuthenticationSessions().keySet());
|
cookie.setRemainingTabs(rootAuthSession.getAuthenticationSessions().keySet());
|
||||||
|
|
|
@ -26,6 +26,7 @@ import org.keycloak.models.ClientModel;
|
||||||
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.models.utils.SessionExpiration;
|
||||||
import org.keycloak.protocol.RestartLoginCookie;
|
import org.keycloak.protocol.RestartLoginCookie;
|
||||||
import org.keycloak.services.resources.LoginActionsService;
|
import org.keycloak.services.resources.LoginActionsService;
|
||||||
import org.keycloak.services.util.CookieHelper;
|
import org.keycloak.services.util.CookieHelper;
|
||||||
|
@ -262,15 +263,22 @@ public class AuthenticationSessionManager {
|
||||||
* @param authSession
|
* @param authSession
|
||||||
*/
|
*/
|
||||||
public void updateAuthenticationSessionAfterSuccessfulAuthentication(RealmModel realm, AuthenticationSessionModel authSession) {
|
public void updateAuthenticationSessionAfterSuccessfulAuthentication(RealmModel realm, AuthenticationSessionModel authSession) {
|
||||||
// TODO: The authentication session might need to be expired in short interval (realm accessCodeLifespan, which is 1 minute by default). That should be sufficient for other browser tabs
|
|
||||||
// to finish authentication and at the same time we won't need to keep authentication sessions in storage longer than needed
|
|
||||||
boolean removedRootAuthSession = removeTabIdInAuthenticationSession(realm, authSession);
|
boolean removedRootAuthSession = removeTabIdInAuthenticationSession(realm, authSession);
|
||||||
if (!removedRootAuthSession) {
|
if (!removedRootAuthSession) {
|
||||||
RootAuthenticationSessionModel rootAuthSession = authSession.getParentSession();
|
RootAuthenticationSessionModel rootAuthSession = authSession.getParentSession();
|
||||||
|
|
||||||
log.tracef("Removed authentication session of root session '%s' with tabId '%s'. But there are remaining tabs in the root session", rootAuthSession.getId(), authSession.getTabId());
|
// 1 minute by default. Same timeout, which is used for client to complete "authorization code" flow
|
||||||
|
// Very short timeout should be OK as when this cookie is set, other existing browser tabs are supposed to be refreshed immediately by JS script authChecker.js
|
||||||
|
// and login user automatically. No need to have authenticationSession and cookie living any longer
|
||||||
|
int authSessionExpiresIn = realm.getAccessCodeLifespan();
|
||||||
|
|
||||||
AuthenticationStateCookie.generateAndSetCookie(session, realm, rootAuthSession);
|
// Set timestamp to the past to make sure that authSession is scheduled for expiration in "authSessionExpiresIn" seconds
|
||||||
|
int authSessionExpirationTime = Time.currentTime() - SessionExpiration.getAuthSessionLifespan(realm) + authSessionExpiresIn;
|
||||||
|
rootAuthSession.setTimestamp(authSessionExpirationTime);
|
||||||
|
|
||||||
|
log.tracef("Removed authentication session of root session '%s' with tabId '%s'. But there are remaining tabs in the root session. Root authentication session will expire in %d seconds", rootAuthSession.getId(), authSession.getTabId(), authSessionExpiresIn);
|
||||||
|
|
||||||
|
AuthenticationStateCookie.generateAndSetCookie(session, realm, rootAuthSession, authSessionExpiresIn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,7 @@ import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.services.resource.RealmResourceProvider;
|
import org.keycloak.services.resource.RealmResourceProvider;
|
||||||
import org.keycloak.services.scheduled.ClearExpiredUserSessions;
|
import org.keycloak.services.scheduled.ClearExpiredUserSessions;
|
||||||
import org.keycloak.services.util.CookieHelper;
|
import org.keycloak.services.util.CookieHelper;
|
||||||
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
import org.keycloak.storage.UserStorageProvider;
|
import org.keycloak.storage.UserStorageProvider;
|
||||||
import org.keycloak.storage.datastore.PeriodicEventInvalidation;
|
import org.keycloak.storage.datastore.PeriodicEventInvalidation;
|
||||||
import org.keycloak.testsuite.components.TestProvider;
|
import org.keycloak.testsuite.components.TestProvider;
|
||||||
|
@ -1106,4 +1107,18 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
||||||
factory.setProvider(this.factory.truststoreProvider);
|
factory.setProvider(this.factory.truststoreProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/get-authentication-session-tabs-count")
|
||||||
|
@NoCache
|
||||||
|
public Integer getAuthenticationSessionTabsCount(@QueryParam("realm") String realmName, @QueryParam("authSessionId") String authSessionId) {
|
||||||
|
RealmModel realm = getRealmByName(realmName);
|
||||||
|
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
||||||
|
if (rootAuthSession == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rootAuthSession.getAuthenticationSessions().size();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -425,4 +425,16 @@ public interface TestingResource {
|
||||||
@NoCache
|
@NoCache
|
||||||
void reenableTruststoreSpi();
|
void reenableTruststoreSpi();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get count of tabs (child authentication sessions) for given "root authentication session"
|
||||||
|
*
|
||||||
|
* @param realm realm name (not ID)
|
||||||
|
* @param authSessionId ID of authentication session
|
||||||
|
* @return count of tabs. Return 0 if authentication session of given ID does not exists (or if it exists, but without any authenticationSessions attached, which should not happen with normal usage)
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("/get-authentication-session-tabs-count")
|
||||||
|
@NoCache
|
||||||
|
Integer getAuthenticationSessionTabsCount(@QueryParam("realm") String realm, @QueryParam("authSessionId") String authSessionId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,6 @@ import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
import org.keycloak.testsuite.AssertEvents;
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
import org.keycloak.testsuite.ProfileAssume;
|
import org.keycloak.testsuite.ProfileAssume;
|
||||||
import org.keycloak.testsuite.admin.ApiUtil;
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
|
|
||||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
import org.keycloak.testsuite.pages.AppPage;
|
import org.keycloak.testsuite.pages.AppPage;
|
||||||
import org.keycloak.testsuite.pages.AppPage.RequestType;
|
import org.keycloak.testsuite.pages.AppPage.RequestType;
|
||||||
|
@ -894,6 +893,28 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent();
|
events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAuthenticationSessionExpiresEarlyAfterAuthentication() throws Exception {
|
||||||
|
// Open login form and refresh right after. This simulates creating another "tab" in rootAuthenticationSession
|
||||||
|
oauth.openLoginForm();
|
||||||
|
driver.navigate().refresh();
|
||||||
|
|
||||||
|
// Assert authenticationSession in cache with 2 tabs
|
||||||
|
String authSessionId = driver.manage().getCookieNamed(AuthenticationSessionManager.AUTH_SESSION_ID).getValue();
|
||||||
|
Assert.assertEquals((Integer) 2, getTestingClient().testing().getAuthenticationSessionTabsCount("test", authSessionId));
|
||||||
|
|
||||||
|
loginPage.login("test-user@localhost", "password");
|
||||||
|
appPage.assertCurrent();
|
||||||
|
|
||||||
|
// authentication session should still exists with remaining browser tab
|
||||||
|
Assert.assertEquals((Integer) 1, getTestingClient().testing().getAuthenticationSessionTabsCount("test", authSessionId));
|
||||||
|
|
||||||
|
// authentication session should be expired after 1 minute
|
||||||
|
setTimeOffset(300);
|
||||||
|
Assert.assertEquals((Integer) 0, getTestingClient().testing().getAuthenticationSessionTabsCount("test", authSessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void loginRememberMeExpiredIdle() throws Exception {
|
public void loginRememberMeExpiredIdle() throws Exception {
|
||||||
try (Closeable c = new RealmAttributeUpdater(adminClient.realm("test"))
|
try (Closeable c = new RealmAttributeUpdater(adminClient.realm("test"))
|
||||||
|
|
Loading…
Reference in a new issue