Use SessionExpirationUtils for validate user and client sessions
Check client session is valid in TokenManager Closes #24936 Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
parent
f32cd91792
commit
f7044ba5c2
10 changed files with 367 additions and 65 deletions
|
@ -328,4 +328,10 @@ one of the future {project_name} releases. It might be {project_name} 27 release
|
||||||
|
|
||||||
`org.keycloak.common.util.Resteasy` has been deprecated. You should use the `org.keycloak.util.KeycloakSessionUtil` to obtain the `KeycloakSession` instead.
|
`org.keycloak.common.util.Resteasy` has been deprecated. You should use the `org.keycloak.util.KeycloakSessionUtil` to obtain the `KeycloakSession` instead.
|
||||||
|
|
||||||
It is highly recommended to avoid obtaining the `KeycloakSession` by means other than when creating your custom provider.
|
It is highly recommended to avoid obtaining the `KeycloakSession` by means other than when creating your custom provider.
|
||||||
|
|
||||||
|
= Small changes in session lifespan and idle calculations
|
||||||
|
|
||||||
|
In previous versions the session max lifespan and idle timeout calculation was slightly different when validating if a session was still valid. Since now that validation uses the same code than the rest of the project.
|
||||||
|
|
||||||
|
If the session is using the remember me feature, the idle timeout and max lifespan are the maximum value between the common SSO and the remember me configuration values.
|
|
@ -21,6 +21,7 @@ import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -319,4 +320,34 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void restartClientSession() {
|
||||||
|
ClientSessionUpdateTask task = new ClientSessionUpdateTask() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void runUpdate(AuthenticatedClientSessionEntity entity) {
|
||||||
|
UserSessionModel userSession = getUserSession();
|
||||||
|
entity.setAction(null);
|
||||||
|
entity.setRedirectUri(null);
|
||||||
|
entity.setCurrentRefreshToken(null);
|
||||||
|
entity.setCurrentRefreshTokenUseCount(-1);
|
||||||
|
entity.setTimestamp(Time.currentTime());
|
||||||
|
entity.getNotes().clear();
|
||||||
|
entity.getNotes().put(AuthenticatedClientSessionModel.STARTED_AT_NOTE, String.valueOf(entity.getTimestamp()));
|
||||||
|
entity.getNotes().put(AuthenticatedClientSessionModel.USER_SESSION_STARTED_AT_NOTE, String.valueOf(userSession.getStarted()));
|
||||||
|
entity.getNotes().put(AuthenticatedClientSessionEntity.CLIENT_ID_NOTE, getClient().getId());
|
||||||
|
if (userSession.isRememberMe()) {
|
||||||
|
entity.getNotes().put(AuthenticatedClientSessionModel.USER_SESSION_REMEMBER_ME_NOTE, "true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOffline() {
|
||||||
|
return offline;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
update(task);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,15 +90,8 @@ public class MergedUpdate<S extends SessionEntity> implements SessionUpdateTask<
|
||||||
result.childUpdates.add(child);
|
result.childUpdates.add(child);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Merge the operations. REMOVE is special case as other operations are not needed then.
|
// Merge the operations.
|
||||||
CacheOperation mergedOp = result.getOperation().merge(child.getOperation(), session);
|
result.operation = result.getOperation().merge(child.getOperation(), session);
|
||||||
if (mergedOp == CacheOperation.REMOVE) {
|
|
||||||
result = new MergedUpdate<>(child.getOperation(), child.getCrossDCMessageStatus(sessionWrapper), lifespanMs, maxIdleTimeMs);
|
|
||||||
result.childUpdates.add(child);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.operation = mergedOp;
|
|
||||||
|
|
||||||
// Check if we need to send message to other DCs and how critical it is
|
// Check if we need to send message to other DCs and how critical it is
|
||||||
CrossDCMessageStatus currentDCStatus = result.getCrossDCMessageStatus(sessionWrapper);
|
CrossDCMessageStatus currentDCStatus = result.getCrossDCMessageStatus(sessionWrapper);
|
||||||
|
@ -109,6 +102,13 @@ public class MergedUpdate<S extends SessionEntity> implements SessionUpdateTask<
|
||||||
result.crossDCMessageStatus = currentDCStatus.merge(childDCStatus);
|
result.crossDCMessageStatus = currentDCStatus.merge(childDCStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// REMOVE is special case as other operations are not needed then.
|
||||||
|
if (result.operation == CacheOperation.REMOVE) {
|
||||||
|
result = new MergedUpdate<>(result.operation, result.crossDCMessageStatus, lifespanMs, maxIdleTimeMs);
|
||||||
|
result.childUpdates.add(child);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// Finally add another update to the result
|
// Finally add another update to the result
|
||||||
result.childUpdates.add(child);
|
result.childUpdates.add(child);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.models;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.sessions.CommonClientSessionModel;
|
import org.keycloak.sessions.CommonClientSessionModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -72,4 +73,20 @@ public interface AuthenticatedClientSessionModel extends CommonClientSessionMode
|
||||||
void setNote(String name, String value);
|
void setNote(String name, String value);
|
||||||
void removeNote(String name);
|
void removeNote(String name);
|
||||||
Map<String, String> getNotes();
|
Map<String, String> getNotes();
|
||||||
|
|
||||||
|
default void restartClientSession() {
|
||||||
|
setAction(null);
|
||||||
|
setRedirectUri(null);
|
||||||
|
setCurrentRefreshToken(null);
|
||||||
|
setCurrentRefreshTokenUseCount(-1);
|
||||||
|
setTimestamp(Time.currentTime());
|
||||||
|
for (String note : getNotes().keySet()) {
|
||||||
|
if (!AuthenticatedClientSessionModel.USER_SESSION_STARTED_AT_NOTE.equals(note)
|
||||||
|
&& !AuthenticatedClientSessionModel.STARTED_AT_NOTE.equals(note)
|
||||||
|
&& !AuthenticatedClientSessionModel.USER_SESSION_REMEMBER_ME_NOTE.equals(note)) {
|
||||||
|
removeNote(note);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getNotes().put(AuthenticatedClientSessionModel.STARTED_AT_NOTE, String.valueOf(getTimestamp()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -153,7 +153,7 @@ public class TokenManager {
|
||||||
if (userSession != null) {
|
if (userSession != null) {
|
||||||
|
|
||||||
// Revoke timeouted offline userSession
|
// Revoke timeouted offline userSession
|
||||||
if (!AuthenticationManager.isOfflineSessionValid(realm, userSession)) {
|
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
|
||||||
sessionManager.revokeOfflineUserSession(userSession);
|
sessionManager.revokeOfflineUserSession(userSession);
|
||||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline session not active", "Offline session not active");
|
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline session not active", "Offline session not active");
|
||||||
}
|
}
|
||||||
|
@ -198,6 +198,12 @@ public class TokenManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!AuthenticationManager.isClientSessionValid(realm, client, userSession, clientSession)) {
|
||||||
|
logger.debug("Client session not active");
|
||||||
|
userSession.removeAuthenticatedClientSessions(Collections.singletonList(client.getId()));
|
||||||
|
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Client session not active");
|
||||||
|
}
|
||||||
|
|
||||||
if (oldToken.isIssuedBeforeSessionStart(clientSession.getStarted())) {
|
if (oldToken.isIssuedBeforeSessionStart(clientSession.getStarted())) {
|
||||||
logger.debug("refresh token issued before the client session started");
|
logger.debug("refresh token issued before the client session started");
|
||||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "refresh token issued before the client session started");
|
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "refresh token issued before the client session started");
|
||||||
|
@ -567,7 +573,10 @@ public class TokenManager {
|
||||||
ClientModel client = authSession.getClient();
|
ClientModel client = authSession.getClient();
|
||||||
|
|
||||||
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
|
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
|
||||||
if (clientSession == null) {
|
if (clientSession != null && !AuthenticationManager.isClientSessionValid(userSession.getRealm(), client, userSession, clientSession)) {
|
||||||
|
// session exists but not active so re-start it
|
||||||
|
clientSession.restartClientSession();
|
||||||
|
} else if (clientSession == null) {
|
||||||
clientSession = session.sessions().createClientSession(userSession.getRealm(), client, userSession);
|
clientSession = session.sessions().createClientSession(userSession.getRealm(), client, userSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,7 @@ import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.models.utils.DefaultRequiredActions;
|
import org.keycloak.models.utils.DefaultRequiredActions;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.models.utils.SessionExpirationUtils;
|
||||||
import org.keycloak.models.utils.SessionTimeoutHelper;
|
import org.keycloak.models.utils.SessionTimeoutHelper;
|
||||||
import org.keycloak.models.utils.SystemClientUtil;
|
import org.keycloak.models.utils.SystemClientUtil;
|
||||||
import org.keycloak.protocol.LoginProtocol;
|
import org.keycloak.protocol.LoginProtocol;
|
||||||
|
@ -107,10 +108,10 @@ import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static org.keycloak.models.light.LightweightUserAdapter.isLightweightUser;
|
|
||||||
import static org.keycloak.models.UserSessionModel.CORRESPONDING_SESSION_ID;
|
import static org.keycloak.models.UserSessionModel.CORRESPONDING_SESSION_ID;
|
||||||
import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.isOAuth2DeviceVerificationFlow;
|
import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.isOAuth2DeviceVerificationFlow;
|
||||||
|
|
||||||
|
@ -175,35 +176,33 @@ public class AuthenticationManager {
|
||||||
logger.debug("No user session");
|
logger.debug("No user session");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
int currentTime = Time.currentTime();
|
long currentTime = Time.currentTimeMillis();
|
||||||
|
long lifespan = SessionExpirationUtils.calculateUserSessionMaxLifespanTimestamp(userSession.isOffline(),
|
||||||
|
userSession.isRememberMe(), TimeUnit.SECONDS.toMillis(userSession.getStarted()), realm);
|
||||||
|
long idle = SessionExpirationUtils.calculateUserSessionIdleTimestamp(userSession.isOffline(),
|
||||||
|
userSession.isRememberMe(), TimeUnit.SECONDS.toMillis(userSession.getLastSessionRefresh()), realm);
|
||||||
|
|
||||||
// Additional time window is added for the case when session was updated in different DC and the update to current DC was postponed
|
boolean sessionIdleOk = idle > currentTime - TimeUnit.SECONDS.toMillis(SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS);
|
||||||
int maxIdle = userSession.isRememberMe() && realm.getSsoSessionIdleTimeoutRememberMe() > 0 ?
|
boolean sessionMaxOk = lifespan == -1L || lifespan > currentTime;
|
||||||
realm.getSsoSessionIdleTimeoutRememberMe() : realm.getSsoSessionIdleTimeout();
|
|
||||||
int maxLifespan = userSession.isRememberMe() && realm.getSsoSessionMaxLifespanRememberMe() > 0 ?
|
|
||||||
realm.getSsoSessionMaxLifespanRememberMe() : realm.getSsoSessionMaxLifespan();
|
|
||||||
|
|
||||||
boolean sessionIdleOk = maxIdle > currentTime - userSession.getLastSessionRefresh() - SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS;
|
|
||||||
boolean sessionMaxOk = maxLifespan > currentTime - userSession.getStarted();
|
|
||||||
return sessionIdleOk && sessionMaxOk;
|
return sessionIdleOk && sessionMaxOk;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isOfflineSessionValid(RealmModel realm, UserSessionModel userSession) {
|
public static boolean isClientSessionValid(RealmModel realm, ClientModel client,
|
||||||
if (userSession == null) {
|
UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
|
||||||
logger.debug("No offline user session");
|
if (userSession == null || clientSession == null) {
|
||||||
|
logger.debug("No user session");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
int currentTime = Time.currentTime();
|
long currentTime = Time.currentTimeMillis();
|
||||||
// Additional time window is added for the case when session was updated in different DC and the update to current DC was postponed
|
long lifespan = SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp(userSession.isOffline(),
|
||||||
int maxIdle = realm.getOfflineSessionIdleTimeout() + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS;
|
userSession.isRememberMe(), TimeUnit.SECONDS.toMillis(clientSession.getStarted()),
|
||||||
|
TimeUnit.SECONDS.toMillis(userSession.getStarted()), realm, client);
|
||||||
|
long idle = SessionExpirationUtils.calculateClientSessionIdleTimestamp(userSession.isOffline(),
|
||||||
|
userSession.isRememberMe(), TimeUnit.SECONDS.toMillis(clientSession.getTimestamp()), realm, client);
|
||||||
|
|
||||||
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
boolean sessionIdleOk = idle > currentTime - TimeUnit.SECONDS.toMillis(SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS);
|
||||||
if (realm.isOfflineSessionMaxLifespanEnabled()) {
|
boolean sessionMaxOk = lifespan == -1L || lifespan > currentTime;
|
||||||
int max = userSession.getStarted() + realm.getOfflineSessionMaxLifespan();
|
return sessionIdleOk && sessionMaxOk;
|
||||||
return userSession.getLastSessionRefresh() + maxIdle > currentTime && max > currentTime;
|
|
||||||
} else {
|
|
||||||
return userSession.getLastSessionRefresh() + maxIdle > currentTime;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean expireUserSessionCookie(KeycloakSession session, UserSessionModel userSession, RealmModel realm, UriInfo uriInfo, HttpHeaders headers, ClientConnection connection) {
|
public static boolean expireUserSessionCookie(KeycloakSession session, UserSessionModel userSession, RealmModel realm, UriInfo uriInfo, HttpHeaders headers, ClientConnection connection) {
|
||||||
|
@ -1427,7 +1426,7 @@ public class AuthenticationManager {
|
||||||
// Check if accessToken was for the offline session.
|
// Check if accessToken was for the offline session.
|
||||||
if (!isCookie) {
|
if (!isCookie) {
|
||||||
UserSessionModel offlineUserSession = session.sessions().getOfflineUserSession(realm, token.getSessionState());
|
UserSessionModel offlineUserSession = session.sessions().getOfflineUserSession(realm, token.getSessionState());
|
||||||
if (isOfflineSessionValid(realm, offlineUserSession)) {
|
if (isSessionValid(realm, offlineUserSession)) {
|
||||||
user = offlineUserSession.getUser();
|
user = offlineUserSession.getUser();
|
||||||
ClientModel client = realm.getClientByClientId(token.getIssuedFor());
|
ClientModel client = realm.getClientByClientId(token.getIssuedFor());
|
||||||
if (!isClientValid(offlineUserSession, client, token)) {
|
if (!isClientValid(offlineUserSession, client, token)) {
|
||||||
|
|
|
@ -54,7 +54,7 @@ public class UserSessionUtil {
|
||||||
return userSession;
|
return userSession;
|
||||||
} else {
|
} else {
|
||||||
offlineUserSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionId(), true, client.getId());
|
offlineUserSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionId(), true, client.getId());
|
||||||
if (AuthenticationManager.isOfflineSessionValid(realm, offlineUserSession)) {
|
if (AuthenticationManager.isSessionValid(realm, offlineUserSession)) {
|
||||||
checkTokenIssuedAt(realm, token, offlineUserSession, event, client);
|
checkTokenIssuedAt(realm, token, offlineUserSession, event, client);
|
||||||
event.session(offlineUserSession);
|
event.session(offlineUserSession);
|
||||||
return offlineUserSession;
|
return offlineUserSession;
|
||||||
|
|
|
@ -918,6 +918,7 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
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"))
|
||||||
.setSsoSessionIdleTimeoutRememberMe(1)
|
.setSsoSessionIdleTimeoutRememberMe(1)
|
||||||
|
.setSsoSessionIdleTimeout(1) // max of both values
|
||||||
.setRememberMe(true)
|
.setRememberMe(true)
|
||||||
.update()) {
|
.update()) {
|
||||||
// login form shown after redirect from app
|
// login form shown after redirect from app
|
||||||
|
|
|
@ -1146,6 +1146,96 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void refreshTokenUserSessionMaxLifespanModifiedAfterTokenRefresh() throws Exception {
|
||||||
|
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||||
|
oauth.clientId("offline-client");
|
||||||
|
oauth.redirectUri(offlineClientAppUri);
|
||||||
|
|
||||||
|
RealmResource realmResource = adminClient.realm("test");
|
||||||
|
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||||
|
|
||||||
|
int[] prev = changeOfflineSessionSettings(true, 7200, 7200, 7200, 7200);
|
||||||
|
try {
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
EventRepresentation loginEvent = events.expectLogin().client("offline-client")
|
||||||
|
.detail(Details.REDIRECT_URI, offlineClientAppUri).assertEvent();
|
||||||
|
|
||||||
|
String sessionId = loginEvent.getSessionId();
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "secret1");
|
||||||
|
assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getType());
|
||||||
|
assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 7200);
|
||||||
|
String clientSessionId = getOfflineClientSessionUuid(sessionId, loginEvent.getClientId());
|
||||||
|
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||||
|
|
||||||
|
events.poll();
|
||||||
|
|
||||||
|
RealmRepresentation rep = realmResource.toRepresentation();
|
||||||
|
rep.setOfflineSessionMaxLifespan(3600);
|
||||||
|
realmResource.update(rep);
|
||||||
|
|
||||||
|
setTimeOffset(3700);
|
||||||
|
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "secret1");
|
||||||
|
assertEquals(400, tokenResponse.getStatusCode());
|
||||||
|
assertNull(tokenResponse.getAccessToken());
|
||||||
|
assertNull(tokenResponse.getRefreshToken());
|
||||||
|
events.expect(EventType.REFRESH_TOKEN).session(sessionId).client("offline-client").error(Errors.INVALID_TOKEN).user((String) null).assertEvent();
|
||||||
|
assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||||
|
} finally {
|
||||||
|
changeOfflineSessionSettings(false, prev[0], prev[1], prev[2], prev[3]);
|
||||||
|
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||||
|
events.clear();
|
||||||
|
resetTimeOffset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void refreshTokenClientSessionMaxLifespanModifiedAfterTokenRefresh() throws Exception {
|
||||||
|
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||||
|
oauth.clientId("offline-client");
|
||||||
|
oauth.redirectUri(offlineClientAppUri);
|
||||||
|
|
||||||
|
RealmResource realmResource = adminClient.realm("test");
|
||||||
|
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||||
|
|
||||||
|
int[] prev = changeOfflineSessionSettings(true, 7200, 7200, 7200, 7200);
|
||||||
|
try {
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
EventRepresentation loginEvent = events.expectLogin().client("offline-client")
|
||||||
|
.detail(Details.REDIRECT_URI, offlineClientAppUri).assertEvent();
|
||||||
|
|
||||||
|
String sessionId = loginEvent.getSessionId();
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "secret1");
|
||||||
|
assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getType());
|
||||||
|
assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 7200);
|
||||||
|
String clientSessionId = getOfflineClientSessionUuid(sessionId, loginEvent.getClientId());
|
||||||
|
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||||
|
|
||||||
|
events.poll();
|
||||||
|
|
||||||
|
RealmRepresentation rep = realmResource.toRepresentation();
|
||||||
|
rep.setClientOfflineSessionMaxLifespan(3600);
|
||||||
|
realmResource.update(rep);
|
||||||
|
|
||||||
|
setTimeOffset(3700);
|
||||||
|
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "secret1");
|
||||||
|
assertEquals(400, tokenResponse.getStatusCode());
|
||||||
|
assertNull(tokenResponse.getAccessToken());
|
||||||
|
assertNull(tokenResponse.getRefreshToken());
|
||||||
|
events.expect(EventType.REFRESH_TOKEN).client("offline-client").error(Errors.INVALID_TOKEN).session(sessionId).user((String) null).assertEvent();
|
||||||
|
assertEquals(1, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||||
|
} finally {
|
||||||
|
changeOfflineSessionSettings(false, prev[0], prev[1], prev[2], prev[3]);
|
||||||
|
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||||
|
events.clear();
|
||||||
|
resetTimeOffset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testShortOfflineSessionMax() throws Exception {
|
public void testShortOfflineSessionMax() throws Exception {
|
||||||
int prevOfflineSession[] = null;
|
int prevOfflineSession[] = null;
|
||||||
|
|
|
@ -1163,14 +1163,13 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||||
@Test
|
@Test
|
||||||
public void testUserSessionRefreshAndIdleRememberMe() throws Exception {
|
public void testUserSessionRefreshAndIdleRememberMe() throws Exception {
|
||||||
RealmResource testRealm = adminClient.realm("test");
|
RealmResource testRealm = adminClient.realm("test");
|
||||||
RealmRepresentation testRealmRep = testRealm.toRepresentation();
|
|
||||||
Boolean previousRememberMe = testRealmRep.isRememberMe();
|
|
||||||
int originalIdleRememberMe = testRealmRep.getSsoSessionIdleTimeoutRememberMe();
|
|
||||||
|
|
||||||
try {
|
|
||||||
testRealmRep.setRememberMe(true);
|
|
||||||
testRealm.update(testRealmRep);
|
|
||||||
|
|
||||||
|
try (Closeable realmUpdater = new RealmAttributeUpdater(testRealm)
|
||||||
|
.updateWith(r -> {
|
||||||
|
r.setRememberMe(true);
|
||||||
|
r.setSsoSessionIdleTimeoutRememberMe(500);
|
||||||
|
r.setSsoSessionIdleTimeout(100);
|
||||||
|
}).update()) {
|
||||||
oauth.doRememberMeLogin("test-user@localhost", "password");
|
oauth.doRememberMeLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||||
|
@ -1185,7 +1184,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||||
String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
|
String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
|
||||||
int last = testingClient.testing().getLastSessionRefresh("test", sessionId, false);
|
int last = testingClient.testing().getLastSessionRefresh("test", sessionId, false);
|
||||||
|
|
||||||
setTimeOffset(2);
|
setTimeOffset(110 + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS);
|
||||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||||
oauth.verifyToken(tokenResponse.getAccessToken());
|
oauth.verifyToken(tokenResponse.getAccessToken());
|
||||||
oauth.parseRefreshToken(tokenResponse.getRefreshToken());
|
oauth.parseRefreshToken(tokenResponse.getRefreshToken());
|
||||||
|
@ -1194,12 +1193,9 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||||
int next = testingClient.testing().getLastSessionRefresh("test", sessionId, false);
|
int next = testingClient.testing().getLastSessionRefresh("test", sessionId, false);
|
||||||
Assert.assertNotEquals(last, next);
|
Assert.assertNotEquals(last, next);
|
||||||
|
|
||||||
testRealmRep.setSsoSessionIdleTimeoutRememberMe(1);
|
|
||||||
testRealm.update(testRealmRep);
|
|
||||||
|
|
||||||
events.clear();
|
events.clear();
|
||||||
// Needs to add some additional time due the tollerance allowed by IDLE_TIMEOUT_WINDOW_SECONDS
|
// Needs to add some additional time due the tollerance allowed by IDLE_TIMEOUT_WINDOW_SECONDS
|
||||||
setTimeOffset(6 + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS);
|
setTimeOffset(620 + 2 * SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS);
|
||||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||||
|
|
||||||
// test idle remember me timeout
|
// test idle remember me timeout
|
||||||
|
@ -1211,9 +1207,6 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||||
events.clear();
|
events.clear();
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
testRealmRep.setSsoSessionIdleTimeoutRememberMe(originalIdleRememberMe);
|
|
||||||
testRealmRep.setRememberMe(previousRememberMe);
|
|
||||||
testRealm.update(testRealmRep);
|
|
||||||
resetTimeOffset();
|
resetTimeOffset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1399,6 +1392,168 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void refreshTokenUserSessionMaxLifespanModifiedAfterTokenRefresh() throws Exception {
|
||||||
|
RealmResource realmResource = adminClient.realm("test");
|
||||||
|
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||||
|
|
||||||
|
try (Closeable realmUpdater = new RealmAttributeUpdater(realmResource)
|
||||||
|
.updateWith(r -> {
|
||||||
|
r.setSsoSessionMaxLifespan(7200);
|
||||||
|
r.setSsoSessionIdleTimeout(7200);
|
||||||
|
r.setClientSessionMaxLifespan(7200);
|
||||||
|
r.setClientSessionIdleTimeout(7200);
|
||||||
|
}).update()) {
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||||
|
|
||||||
|
String sessionId = loginEvent.getSessionId();
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||||
|
assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 7200);
|
||||||
|
final String clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
|
||||||
|
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||||
|
|
||||||
|
events.poll();
|
||||||
|
|
||||||
|
RealmRepresentation rep = realmResource.toRepresentation();
|
||||||
|
rep.setSsoSessionMaxLifespan(3600);
|
||||||
|
realmResource.update(rep);
|
||||||
|
|
||||||
|
setTimeOffset(3700);
|
||||||
|
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||||
|
assertEquals(400, tokenResponse.getStatusCode());
|
||||||
|
assertNull(tokenResponse.getAccessToken());
|
||||||
|
assertNull(tokenResponse.getRefreshToken());
|
||||||
|
events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).session(sessionId).user((String) null).assertEvent();
|
||||||
|
assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||||
|
} finally {
|
||||||
|
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||||
|
events.clear();
|
||||||
|
resetTimeOffset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void refreshTokenClientSessionMaxLifespanModifiedAfterTokenRefresh() throws Exception {
|
||||||
|
RealmResource realmResource = adminClient.realm("test");
|
||||||
|
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||||
|
|
||||||
|
try (Closeable realmUpdater = new RealmAttributeUpdater(realmResource)
|
||||||
|
.updateWith(r -> {
|
||||||
|
r.setSsoSessionMaxLifespan(7200);
|
||||||
|
r.setSsoSessionIdleTimeout(7200);
|
||||||
|
r.setClientSessionMaxLifespan(7200);
|
||||||
|
r.setClientSessionIdleTimeout(7200);
|
||||||
|
}).update()) {
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||||
|
|
||||||
|
String sessionId = loginEvent.getSessionId();
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||||
|
assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 7200);
|
||||||
|
String clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
|
||||||
|
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||||
|
|
||||||
|
events.poll();
|
||||||
|
|
||||||
|
RealmRepresentation rep = realmResource.toRepresentation();
|
||||||
|
rep.setClientSessionMaxLifespan(3600);
|
||||||
|
realmResource.update(rep);
|
||||||
|
|
||||||
|
setTimeOffset(3700);
|
||||||
|
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||||
|
assertEquals(400, tokenResponse.getStatusCode());
|
||||||
|
assertNull(tokenResponse.getAccessToken());
|
||||||
|
assertNull(tokenResponse.getRefreshToken());
|
||||||
|
events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).session(sessionId).user((String) null).assertEvent();
|
||||||
|
assertEquals(1, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||||
|
|
||||||
|
setTimeOffset(4200);
|
||||||
|
oauth.doSilentLogin();
|
||||||
|
loginEvent = events.expectLogin().assertEvent();
|
||||||
|
sessionId = loginEvent.getSessionId();
|
||||||
|
code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||||
|
assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 3000);
|
||||||
|
events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), sessionId).assertEvent();
|
||||||
|
|
||||||
|
clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
|
||||||
|
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||||
|
|
||||||
|
setTimeOffset(7300);
|
||||||
|
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||||
|
assertEquals(400, tokenResponse.getStatusCode());
|
||||||
|
assertNull(tokenResponse.getAccessToken());
|
||||||
|
assertNull(tokenResponse.getRefreshToken());
|
||||||
|
events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).user((String) null).assertEvent();
|
||||||
|
assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||||
|
} finally {
|
||||||
|
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||||
|
events.clear();
|
||||||
|
resetTimeOffset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void silentLoginClientSessionMaxLifespanModifiedAfterTokenRefresh() throws Exception {
|
||||||
|
RealmResource realmResource = adminClient.realm("test");
|
||||||
|
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||||
|
|
||||||
|
try (Closeable realmUpdater = new RealmAttributeUpdater(realmResource)
|
||||||
|
.updateWith(r -> {
|
||||||
|
r.setSsoSessionMaxLifespan(7200);
|
||||||
|
r.setSsoSessionIdleTimeout(7200);
|
||||||
|
r.setClientSessionMaxLifespan(7200);
|
||||||
|
r.setClientSessionIdleTimeout(7200);
|
||||||
|
}).update()) {
|
||||||
|
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||||
|
|
||||||
|
String sessionId = loginEvent.getSessionId();
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||||
|
assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 7200);
|
||||||
|
String clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
|
||||||
|
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||||
|
|
||||||
|
events.poll();
|
||||||
|
|
||||||
|
RealmRepresentation rep = realmResource.toRepresentation();
|
||||||
|
rep.setClientSessionMaxLifespan(3600);
|
||||||
|
realmResource.update(rep);
|
||||||
|
|
||||||
|
setTimeOffset(4200);
|
||||||
|
oauth.doSilentLogin();
|
||||||
|
loginEvent = events.expectLogin().assertEvent();
|
||||||
|
sessionId = loginEvent.getSessionId();
|
||||||
|
code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||||
|
assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 3000);
|
||||||
|
events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), sessionId).assertEvent();
|
||||||
|
|
||||||
|
clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
|
||||||
|
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||||
|
|
||||||
|
setTimeOffset(7300);
|
||||||
|
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||||
|
assertEquals(400, tokenResponse.getStatusCode());
|
||||||
|
assertNull(tokenResponse.getAccessToken());
|
||||||
|
assertNull(tokenResponse.getRefreshToken());
|
||||||
|
events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).user((String) null).assertEvent();
|
||||||
|
assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||||
|
} finally {
|
||||||
|
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||||
|
events.clear();
|
||||||
|
resetTimeOffset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KEYCLOAK-1267
|
* KEYCLOAK-1267
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
|
@ -1407,13 +1562,13 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||||
public void refreshTokenUserSessionMaxLifespanWithRememberMe() throws Exception {
|
public void refreshTokenUserSessionMaxLifespanWithRememberMe() throws Exception {
|
||||||
|
|
||||||
RealmResource testRealm = adminClient.realm("test");
|
RealmResource testRealm = adminClient.realm("test");
|
||||||
RealmRepresentation testRealmRep = testRealm.toRepresentation();
|
|
||||||
Boolean previousRememberMe = testRealmRep.isRememberMe();
|
|
||||||
int previousSsoMaxLifespanRememberMe = testRealmRep.getSsoSessionMaxLifespanRememberMe();
|
|
||||||
|
|
||||||
try {
|
try (Closeable realmUpdater = new RealmAttributeUpdater(testRealm)
|
||||||
testRealmRep.setRememberMe(true);
|
.updateWith(r -> {
|
||||||
testRealm.update(testRealmRep);
|
r.setRememberMe(true);
|
||||||
|
r.setSsoSessionMaxLifespanRememberMe(100);
|
||||||
|
r.setSsoSessionMaxLifespan(50);
|
||||||
|
}).update()) {
|
||||||
|
|
||||||
oauth.doRememberMeLogin("test-user@localhost", "password");
|
oauth.doRememberMeLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
|
@ -1428,10 +1583,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||||
|
|
||||||
String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
|
String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
|
||||||
|
|
||||||
testRealmRep.setSsoSessionMaxLifespanRememberMe(1);
|
setTimeOffset(110);
|
||||||
testRealm.update(testRealmRep);
|
|
||||||
|
|
||||||
setTimeOffset(2);
|
|
||||||
|
|
||||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||||
|
|
||||||
|
@ -1443,9 +1595,6 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||||
events.clear();
|
events.clear();
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
testRealmRep.setSsoSessionMaxLifespanRememberMe(previousSsoMaxLifespanRememberMe);
|
|
||||||
testRealmRep.setRememberMe(previousRememberMe);
|
|
||||||
testRealm.update(testRealmRep);
|
|
||||||
resetTimeOffset();
|
resetTimeOffset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue