KEYCLOAK-7688 Offline Session Max for Offline Token
This commit is contained in:
parent
b478472b35
commit
2fb022e501
16 changed files with 259 additions and 2 deletions
|
@ -44,6 +44,9 @@ public class RealmRepresentation {
|
||||||
protected Integer ssoSessionIdleTimeout;
|
protected Integer ssoSessionIdleTimeout;
|
||||||
protected Integer ssoSessionMaxLifespan;
|
protected Integer ssoSessionMaxLifespan;
|
||||||
protected Integer offlineSessionIdleTimeout;
|
protected Integer offlineSessionIdleTimeout;
|
||||||
|
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||||
|
protected Boolean offlineSessionMaxLifespanEnabled;
|
||||||
|
protected Integer offlineSessionMaxLifespan;
|
||||||
protected Integer accessCodeLifespan;
|
protected Integer accessCodeLifespan;
|
||||||
protected Integer accessCodeLifespanUserAction;
|
protected Integer accessCodeLifespanUserAction;
|
||||||
protected Integer accessCodeLifespanLogin;
|
protected Integer accessCodeLifespanLogin;
|
||||||
|
@ -296,6 +299,23 @@ public class RealmRepresentation {
|
||||||
this.offlineSessionIdleTimeout = offlineSessionIdleTimeout;
|
this.offlineSessionIdleTimeout = offlineSessionIdleTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||||
|
public Boolean getOfflineSessionMaxLifespanEnabled() {
|
||||||
|
return offlineSessionMaxLifespanEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOfflineSessionMaxLifespanEnabled(Boolean offlineSessionMaxLifespanEnabled) {
|
||||||
|
this.offlineSessionMaxLifespanEnabled = offlineSessionMaxLifespanEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getOfflineSessionMaxLifespan() {
|
||||||
|
return offlineSessionMaxLifespan;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOfflineSessionMaxLifespan(Integer offlineSessionMaxLifespan) {
|
||||||
|
this.offlineSessionMaxLifespan = offlineSessionMaxLifespan;
|
||||||
|
}
|
||||||
|
|
||||||
public List<ScopeMappingRepresentation> getScopeMappings() {
|
public List<ScopeMappingRepresentation> getScopeMappings() {
|
||||||
return scopeMappings;
|
return scopeMappings;
|
||||||
}
|
}
|
||||||
|
|
|
@ -418,6 +418,31 @@ public class RealmAdapter implements CachedRealmModel {
|
||||||
updated.setOfflineSessionIdleTimeout(seconds);
|
updated.setOfflineSessionIdleTimeout(seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||||
|
@Override
|
||||||
|
public boolean isOfflineSessionMaxLifespanEnabled() {
|
||||||
|
if (isUpdated()) return updated.isOfflineSessionMaxLifespanEnabled();
|
||||||
|
return cached.isOfflineSessionMaxLifespanEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOfflineSessionMaxLifespanEnabled(boolean offlineSessionMaxLifespanEnabled) {
|
||||||
|
getDelegateForUpdate();
|
||||||
|
updated.setOfflineSessionMaxLifespanEnabled(offlineSessionMaxLifespanEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOfflineSessionMaxLifespan() {
|
||||||
|
if (isUpdated()) return updated.getOfflineSessionMaxLifespan();
|
||||||
|
return cached.getOfflineSessionMaxLifespan();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOfflineSessionMaxLifespan(int seconds) {
|
||||||
|
getDelegateForUpdate();
|
||||||
|
updated.setOfflineSessionMaxLifespan(seconds);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAccessTokenLifespan() {
|
public int getAccessTokenLifespan() {
|
||||||
if (isUpdated()) return updated.getAccessTokenLifespan();
|
if (isUpdated()) return updated.getAccessTokenLifespan();
|
||||||
|
|
|
@ -79,6 +79,9 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
||||||
protected int ssoSessionIdleTimeout;
|
protected int ssoSessionIdleTimeout;
|
||||||
protected int ssoSessionMaxLifespan;
|
protected int ssoSessionMaxLifespan;
|
||||||
protected int offlineSessionIdleTimeout;
|
protected int offlineSessionIdleTimeout;
|
||||||
|
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||||
|
protected boolean offlineSessionMaxLifespanEnabled;
|
||||||
|
protected int offlineSessionMaxLifespan;
|
||||||
protected int accessTokenLifespan;
|
protected int accessTokenLifespan;
|
||||||
protected int accessTokenLifespanForImplicitFlow;
|
protected int accessTokenLifespanForImplicitFlow;
|
||||||
protected int accessCodeLifespan;
|
protected int accessCodeLifespan;
|
||||||
|
@ -181,6 +184,9 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
||||||
ssoSessionIdleTimeout = model.getSsoSessionIdleTimeout();
|
ssoSessionIdleTimeout = model.getSsoSessionIdleTimeout();
|
||||||
ssoSessionMaxLifespan = model.getSsoSessionMaxLifespan();
|
ssoSessionMaxLifespan = model.getSsoSessionMaxLifespan();
|
||||||
offlineSessionIdleTimeout = model.getOfflineSessionIdleTimeout();
|
offlineSessionIdleTimeout = model.getOfflineSessionIdleTimeout();
|
||||||
|
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||||
|
offlineSessionMaxLifespanEnabled = model.isOfflineSessionMaxLifespanEnabled();
|
||||||
|
offlineSessionMaxLifespan = model.getOfflineSessionMaxLifespan();
|
||||||
accessTokenLifespan = model.getAccessTokenLifespan();
|
accessTokenLifespan = model.getAccessTokenLifespan();
|
||||||
accessTokenLifespanForImplicitFlow = model.getAccessTokenLifespanForImplicitFlow();
|
accessTokenLifespanForImplicitFlow = model.getAccessTokenLifespanForImplicitFlow();
|
||||||
accessCodeLifespan = model.getAccessCodeLifespan();
|
accessCodeLifespan = model.getAccessCodeLifespan();
|
||||||
|
@ -405,6 +411,15 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
||||||
return offlineSessionIdleTimeout;
|
return offlineSessionIdleTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||||
|
public boolean isOfflineSessionMaxLifespanEnabled() {
|
||||||
|
return offlineSessionMaxLifespanEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOfflineSessionMaxLifespan() {
|
||||||
|
return offlineSessionMaxLifespan;
|
||||||
|
}
|
||||||
|
|
||||||
public int getAccessTokenLifespan() {
|
public int getAccessTokenLifespan() {
|
||||||
return accessTokenLifespan;
|
return accessTokenLifespan;
|
||||||
}
|
}
|
||||||
|
|
|
@ -466,6 +466,27 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
||||||
realm.setOfflineSessionIdleTimeout(seconds);
|
realm.setOfflineSessionIdleTimeout(seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||||
|
@Override
|
||||||
|
public boolean isOfflineSessionMaxLifespanEnabled() {
|
||||||
|
return getAttribute(RealmAttributes.OFFLINE_SESSION_MAX_LIFESPAN_ENABLED, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOfflineSessionMaxLifespanEnabled(boolean offlineSessionMaxLifespanEnabled) {
|
||||||
|
setAttribute(RealmAttributes.OFFLINE_SESSION_MAX_LIFESPAN_ENABLED, offlineSessionMaxLifespanEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOfflineSessionMaxLifespan() {
|
||||||
|
return getAttribute(RealmAttributes.OFFLINE_SESSION_MAX_LIFESPAN, Constants.DEFAULT_OFFLINE_SESSION_MAX_LIFESPAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOfflineSessionMaxLifespan(int seconds) {
|
||||||
|
setAttribute(RealmAttributes.OFFLINE_SESSION_MAX_LIFESPAN, seconds);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAccessCodeLifespan() {
|
public int getAccessCodeLifespan() {
|
||||||
return realm.getAccessCodeLifespan();
|
return realm.getAccessCodeLifespan();
|
||||||
|
|
|
@ -30,4 +30,8 @@ public interface RealmAttributes {
|
||||||
|
|
||||||
String ACTION_TOKEN_GENERATED_BY_USER_LIFESPAN = "actionTokenGeneratedByUserLifespan";
|
String ACTION_TOKEN_GENERATED_BY_USER_LIFESPAN = "actionTokenGeneratedByUserLifespan";
|
||||||
|
|
||||||
|
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||||
|
String OFFLINE_SESSION_MAX_LIFESPAN_ENABLED = "offlineSessionMaxLifespanEnabled";
|
||||||
|
|
||||||
|
String OFFLINE_SESSION_MAX_LIFESPAN = "offlineSessionMaxLifespan";
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,9 @@ public interface Constants {
|
||||||
int DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT = 900;
|
int DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT = 900;
|
||||||
// 30 days
|
// 30 days
|
||||||
int DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT = 2592000;
|
int DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT = 2592000;
|
||||||
|
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||||
|
// 60 days
|
||||||
|
int DEFAULT_OFFLINE_SESSION_MAX_LIFESPAN = 5184000;
|
||||||
|
|
||||||
String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
|
String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
|
||||||
String VERIFY_EMAIL_CODE = "VERIFY_EMAIL_CODE";
|
String VERIFY_EMAIL_CODE = "VERIFY_EMAIL_CODE";
|
||||||
|
|
|
@ -265,6 +265,9 @@ public class ModelToRepresentation {
|
||||||
rep.setSsoSessionIdleTimeout(realm.getSsoSessionIdleTimeout());
|
rep.setSsoSessionIdleTimeout(realm.getSsoSessionIdleTimeout());
|
||||||
rep.setSsoSessionMaxLifespan(realm.getSsoSessionMaxLifespan());
|
rep.setSsoSessionMaxLifespan(realm.getSsoSessionMaxLifespan());
|
||||||
rep.setOfflineSessionIdleTimeout(realm.getOfflineSessionIdleTimeout());
|
rep.setOfflineSessionIdleTimeout(realm.getOfflineSessionIdleTimeout());
|
||||||
|
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||||
|
rep.setOfflineSessionMaxLifespanEnabled(realm.isOfflineSessionMaxLifespanEnabled());
|
||||||
|
rep.setOfflineSessionMaxLifespan(realm.getOfflineSessionMaxLifespan());
|
||||||
rep.setAccessCodeLifespan(realm.getAccessCodeLifespan());
|
rep.setAccessCodeLifespan(realm.getAccessCodeLifespan());
|
||||||
rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
|
rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
|
||||||
rep.setAccessCodeLifespanLogin(realm.getAccessCodeLifespanLogin());
|
rep.setAccessCodeLifespanLogin(realm.getAccessCodeLifespanLogin());
|
||||||
|
|
|
@ -196,6 +196,14 @@ public class RepresentationToModel {
|
||||||
newRealm.setOfflineSessionIdleTimeout(rep.getOfflineSessionIdleTimeout());
|
newRealm.setOfflineSessionIdleTimeout(rep.getOfflineSessionIdleTimeout());
|
||||||
else newRealm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT);
|
else newRealm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT);
|
||||||
|
|
||||||
|
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||||
|
if (rep.getOfflineSessionMaxLifespanEnabled() != null) newRealm.setOfflineSessionMaxLifespanEnabled(rep.getOfflineSessionMaxLifespanEnabled());
|
||||||
|
else newRealm.setOfflineSessionMaxLifespanEnabled(false);
|
||||||
|
|
||||||
|
if (rep.getOfflineSessionMaxLifespan() != null)
|
||||||
|
newRealm.setOfflineSessionMaxLifespan(rep.getOfflineSessionMaxLifespan());
|
||||||
|
else newRealm.setOfflineSessionMaxLifespan(Constants.DEFAULT_OFFLINE_SESSION_MAX_LIFESPAN);
|
||||||
|
|
||||||
if (rep.getAccessCodeLifespan() != null) newRealm.setAccessCodeLifespan(rep.getAccessCodeLifespan());
|
if (rep.getAccessCodeLifespan() != null) newRealm.setAccessCodeLifespan(rep.getAccessCodeLifespan());
|
||||||
else newRealm.setAccessCodeLifespan(60);
|
else newRealm.setAccessCodeLifespan(60);
|
||||||
|
|
||||||
|
@ -906,6 +914,10 @@ public class RepresentationToModel {
|
||||||
if (rep.getSsoSessionMaxLifespan() != null) realm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan());
|
if (rep.getSsoSessionMaxLifespan() != null) realm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan());
|
||||||
if (rep.getOfflineSessionIdleTimeout() != null)
|
if (rep.getOfflineSessionIdleTimeout() != null)
|
||||||
realm.setOfflineSessionIdleTimeout(rep.getOfflineSessionIdleTimeout());
|
realm.setOfflineSessionIdleTimeout(rep.getOfflineSessionIdleTimeout());
|
||||||
|
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||||
|
if (rep.getOfflineSessionMaxLifespanEnabled() != null) realm.setOfflineSessionMaxLifespanEnabled(rep.getOfflineSessionMaxLifespanEnabled());
|
||||||
|
if (rep.getOfflineSessionMaxLifespan() != null)
|
||||||
|
realm.setOfflineSessionMaxLifespan(rep.getOfflineSessionMaxLifespan());
|
||||||
if (rep.getRequiredCredentials() != null) {
|
if (rep.getRequiredCredentials() != null) {
|
||||||
realm.updateRequiredCredentials(rep.getRequiredCredentials());
|
realm.updateRequiredCredentials(rep.getRequiredCredentials());
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,6 +179,13 @@ public interface RealmModel extends RoleContainerModel {
|
||||||
|
|
||||||
int getAccessTokenLifespan();
|
int getAccessTokenLifespan();
|
||||||
|
|
||||||
|
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||||
|
boolean isOfflineSessionMaxLifespanEnabled();
|
||||||
|
void setOfflineSessionMaxLifespanEnabled(boolean offlineSessionMaxLifespanEnabled);
|
||||||
|
|
||||||
|
int getOfflineSessionMaxLifespan();
|
||||||
|
void setOfflineSessionMaxLifespan(int seconds);
|
||||||
|
|
||||||
void setAccessTokenLifespan(int seconds);
|
void setAccessTokenLifespan(int seconds);
|
||||||
|
|
||||||
int getAccessTokenLifespanForImplicitFlow();
|
int getAccessTokenLifespanForImplicitFlow();
|
||||||
|
|
|
@ -76,6 +76,9 @@ public class ApplianceBootstrap {
|
||||||
realm.setAccessTokenLifespanForImplicitFlow(Constants.DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT);
|
realm.setAccessTokenLifespanForImplicitFlow(Constants.DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT);
|
||||||
realm.setSsoSessionMaxLifespan(36000);
|
realm.setSsoSessionMaxLifespan(36000);
|
||||||
realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT);
|
realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT);
|
||||||
|
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||||
|
realm.setOfflineSessionMaxLifespanEnabled(false);
|
||||||
|
realm.setOfflineSessionMaxLifespan(Constants.DEFAULT_OFFLINE_SESSION_MAX_LIFESPAN);
|
||||||
realm.setAccessCodeLifespan(60);
|
realm.setAccessCodeLifespan(60);
|
||||||
realm.setAccessCodeLifespanUserAction(300);
|
realm.setAccessCodeLifespanUserAction(300);
|
||||||
realm.setAccessCodeLifespanLogin(1800);
|
realm.setAccessCodeLifespanLogin(1800);
|
||||||
|
|
|
@ -118,11 +118,16 @@ public class AuthenticationManager {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
int currentTime = Time.currentTime();
|
int currentTime = Time.currentTime();
|
||||||
|
|
||||||
// Additional time window is added for the case when session was updated in different DC and the update to current DC was postponed
|
// Additional time window is added for the case when session was updated in different DC and the update to current DC was postponed
|
||||||
int maxIdle = realm.getOfflineSessionIdleTimeout() + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS;
|
int maxIdle = realm.getOfflineSessionIdleTimeout() + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS;
|
||||||
|
|
||||||
return userSession.getLastSessionRefresh() + maxIdle > currentTime;
|
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||||
|
if (realm.isOfflineSessionMaxLifespanEnabled()) {
|
||||||
|
int max = userSession.getStarted() + realm.getOfflineSessionMaxLifespan();
|
||||||
|
return userSession.getLastSessionRefresh() + maxIdle > currentTime && max > currentTime;
|
||||||
|
} else {
|
||||||
|
return userSession.getLastSessionRefresh() + maxIdle > currentTime;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void expireUserSessionCookie(KeycloakSession session, UserSessionModel userSession, RealmModel realm, UriInfo uriInfo, HttpHeaders headers, ClientConnection connection) {
|
public static void expireUserSessionCookie(KeycloakSession session, UserSessionModel userSession, RealmModel realm, UriInfo uriInfo, HttpHeaders headers, ClientConnection connection) {
|
||||||
|
|
|
@ -30,11 +30,13 @@ import org.keycloak.admin.client.resource.RealmResource;
|
||||||
import org.keycloak.admin.client.resource.RoleResource;
|
import org.keycloak.admin.client.resource.RoleResource;
|
||||||
import org.keycloak.admin.client.resource.UserResource;
|
import org.keycloak.admin.client.resource.UserResource;
|
||||||
import org.keycloak.common.constants.ServiceAccountConstants;
|
import org.keycloak.common.constants.ServiceAccountConstants;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.models.AdminRoles;
|
import org.keycloak.models.AdminRoles;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.models.utils.SessionTimeoutHelper;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.RefreshToken;
|
import org.keycloak.representations.RefreshToken;
|
||||||
|
@ -661,4 +663,88 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||||
assertNotEquals(offlineToken.getSessionState(), offlineToken2.getSessionState());
|
assertNotEquals(offlineToken.getSessionState(), offlineToken2.getSessionState());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||||
|
private int[] changeOfflineSessionSettings(boolean isEnabled, int sessionMax, int sessionIdle) {
|
||||||
|
int prev[] = new int[2];
|
||||||
|
RealmRepresentation rep = adminClient.realm("test").toRepresentation();
|
||||||
|
prev[0] = rep.getOfflineSessionMaxLifespan().intValue();
|
||||||
|
prev[1] = rep.getOfflineSessionIdleTimeout().intValue();
|
||||||
|
RealmBuilder realmBuilder = RealmBuilder.create();
|
||||||
|
realmBuilder.offlineSessionMaxLifespanEnabled(isEnabled).offlineSessionMaxLifespan(sessionMax).offlineSessionIdleTimeout(sessionIdle);
|
||||||
|
adminClient.realm("test").update(realmBuilder.build());
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void offlineTokenBrowserFlowMaxLifespanExpired() throws Exception {
|
||||||
|
// expect that offline session expired by max lifespan
|
||||||
|
final int MAX_LIFESPAN = 3600;
|
||||||
|
final int IDLE_LIFESPAN = 6000;
|
||||||
|
testOfflineSessionExpiration(IDLE_LIFESPAN, MAX_LIFESPAN, MAX_LIFESPAN + 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void offlineTokenBrowserFlowIdleTimeExpired() throws Exception {
|
||||||
|
// expect that offline session expired by idle time
|
||||||
|
final int MAX_LIFESPAN = 3000;
|
||||||
|
final int IDLE_LIFESPAN = 600;
|
||||||
|
// Additional time window is added for the case when session was updated in different DC and the update to current DC was postponed
|
||||||
|
testOfflineSessionExpiration(IDLE_LIFESPAN, MAX_LIFESPAN, IDLE_LIFESPAN + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS + 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testOfflineSessionExpiration(int idleTime, int maxLifespan, int offset) {
|
||||||
|
int prev[] = null;
|
||||||
|
try {
|
||||||
|
prev = changeOfflineSessionSettings(true, maxLifespan, idleTime);
|
||||||
|
|
||||||
|
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||||
|
oauth.clientId("offline-client");
|
||||||
|
oauth.redirectUri(offlineClientAppUri);
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
|
EventRepresentation loginEvent = events.expectLogin()
|
||||||
|
.client("offline-client")
|
||||||
|
.detail(Details.REDIRECT_URI, offlineClientAppUri)
|
||||||
|
.assertEvent();
|
||||||
|
|
||||||
|
final String sessionId = loginEvent.getSessionId();
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "secret1");
|
||||||
|
String offlineTokenString = tokenResponse.getRefreshToken();
|
||||||
|
RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString);
|
||||||
|
|
||||||
|
assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
|
||||||
|
|
||||||
|
tokenResponse = oauth.doRefreshTokenRequest(offlineTokenString, "secret1");
|
||||||
|
AccessToken refreshedToken = oauth.verifyToken(tokenResponse.getAccessToken());
|
||||||
|
offlineTokenString = tokenResponse.getRefreshToken();
|
||||||
|
offlineToken = oauth.verifyRefreshToken(offlineTokenString);
|
||||||
|
|
||||||
|
Assert.assertEquals(200, tokenResponse.getStatusCode());
|
||||||
|
Assert.assertEquals(sessionId, refreshedToken.getSessionState());
|
||||||
|
|
||||||
|
// wait to expire
|
||||||
|
setTimeOffset(offset);
|
||||||
|
|
||||||
|
tokenResponse = oauth.doRefreshTokenRequest(offlineTokenString, "secret1");
|
||||||
|
|
||||||
|
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||||
|
assertEquals("invalid_grant", tokenResponse.getError());
|
||||||
|
|
||||||
|
// Assert userSession expired
|
||||||
|
testingClient.testing().removeExpired("test");
|
||||||
|
try {
|
||||||
|
testingClient.testing().removeUserSession("test", sessionId);
|
||||||
|
} catch (NotFoundException nfe) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeOffset(0);
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
changeOfflineSessionSettings(false, prev[0], prev[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -225,4 +225,20 @@ public class RealmBuilder {
|
||||||
rep.getGroups().add(group);
|
rep.getGroups().add(group);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||||
|
public RealmBuilder offlineSessionIdleTimeout(int offlineSessionIdleTimeout) {
|
||||||
|
rep.setOfflineSessionIdleTimeout(offlineSessionIdleTimeout);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmBuilder offlineSessionMaxLifespan(int offlineSessionMaxLifespan) {
|
||||||
|
rep.setOfflineSessionMaxLifespan(offlineSessionMaxLifespan);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmBuilder offlineSessionMaxLifespanEnabled(boolean offlineSessionMaxLifespanEnabled) {
|
||||||
|
rep.setOfflineSessionMaxLifespanEnabled(offlineSessionMaxLifespanEnabled);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,6 +108,13 @@ sso-session-idle.tooltip=Time a session is allowed to be idle before it expires.
|
||||||
sso-session-max.tooltip=Max time before a session is expired. Tokens and browser sessions are invalidated when a session is expired.
|
sso-session-max.tooltip=Max time before a session is expired. Tokens and browser sessions are invalidated when a session is expired.
|
||||||
offline-session-idle=Offline Session Idle
|
offline-session-idle=Offline Session Idle
|
||||||
offline-session-idle.tooltip=Time an offline session is allowed to be idle before it expires. You need to use offline token to refresh at least once within this period, otherwise offline session will expire.
|
offline-session-idle.tooltip=Time an offline session is allowed to be idle before it expires. You need to use offline token to refresh at least once within this period, otherwise offline session will expire.
|
||||||
|
|
||||||
|
## KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||||
|
offline-session-max-limited=Offline Session Max Limited
|
||||||
|
offline-session-max-limited.tooltip=Enable Offline Session Max.
|
||||||
|
offline-session-max=Offline Session Max
|
||||||
|
offline-session-max.tooltip=Max time before an offline session is expired regardless of activity.
|
||||||
|
|
||||||
access-token-lifespan=Access Token Lifespan
|
access-token-lifespan=Access Token Lifespan
|
||||||
access-token-lifespan.tooltip=Max time before an access token is expired. This value is recommended to be short relative to the SSO timeout.
|
access-token-lifespan.tooltip=Max time before an access token is expired. This value is recommended to be short relative to the SSO timeout.
|
||||||
access-token-lifespan-for-implicit-flow=Access Token Lifespan For Implicit Flow
|
access-token-lifespan-for-implicit-flow=Access Token Lifespan For Implicit Flow
|
||||||
|
|
|
@ -1090,6 +1090,8 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
|
||||||
$scope.realm.ssoSessionIdleTimeout = TimeUnit2.asUnit(realm.ssoSessionIdleTimeout);
|
$scope.realm.ssoSessionIdleTimeout = TimeUnit2.asUnit(realm.ssoSessionIdleTimeout);
|
||||||
$scope.realm.ssoSessionMaxLifespan = TimeUnit2.asUnit(realm.ssoSessionMaxLifespan);
|
$scope.realm.ssoSessionMaxLifespan = TimeUnit2.asUnit(realm.ssoSessionMaxLifespan);
|
||||||
$scope.realm.offlineSessionIdleTimeout = TimeUnit2.asUnit(realm.offlineSessionIdleTimeout);
|
$scope.realm.offlineSessionIdleTimeout = TimeUnit2.asUnit(realm.offlineSessionIdleTimeout);
|
||||||
|
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||||
|
$scope.realm.offlineSessionMaxLifespan = TimeUnit2.asUnit(realm.offlineSessionMaxLifespan);
|
||||||
$scope.realm.accessCodeLifespan = TimeUnit2.asUnit(realm.accessCodeLifespan);
|
$scope.realm.accessCodeLifespan = TimeUnit2.asUnit(realm.accessCodeLifespan);
|
||||||
$scope.realm.accessCodeLifespanLogin = TimeUnit2.asUnit(realm.accessCodeLifespanLogin);
|
$scope.realm.accessCodeLifespanLogin = TimeUnit2.asUnit(realm.accessCodeLifespanLogin);
|
||||||
$scope.realm.accessCodeLifespanUserAction = TimeUnit2.asUnit(realm.accessCodeLifespanUserAction);
|
$scope.realm.accessCodeLifespanUserAction = TimeUnit2.asUnit(realm.accessCodeLifespanUserAction);
|
||||||
|
@ -1137,6 +1139,8 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
|
||||||
$scope.realm.ssoSessionIdleTimeout = $scope.realm.ssoSessionIdleTimeout.toSeconds();
|
$scope.realm.ssoSessionIdleTimeout = $scope.realm.ssoSessionIdleTimeout.toSeconds();
|
||||||
$scope.realm.ssoSessionMaxLifespan = $scope.realm.ssoSessionMaxLifespan.toSeconds();
|
$scope.realm.ssoSessionMaxLifespan = $scope.realm.ssoSessionMaxLifespan.toSeconds();
|
||||||
$scope.realm.offlineSessionIdleTimeout = $scope.realm.offlineSessionIdleTimeout.toSeconds();
|
$scope.realm.offlineSessionIdleTimeout = $scope.realm.offlineSessionIdleTimeout.toSeconds();
|
||||||
|
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||||
|
$scope.realm.offlineSessionMaxLifespan = $scope.realm.offlineSessionMaxLifespan.toSeconds();
|
||||||
$scope.realm.accessCodeLifespan = $scope.realm.accessCodeLifespan.toSeconds();
|
$scope.realm.accessCodeLifespan = $scope.realm.accessCodeLifespan.toSeconds();
|
||||||
$scope.realm.accessCodeLifespanUserAction = $scope.realm.accessCodeLifespanUserAction.toSeconds();
|
$scope.realm.accessCodeLifespanUserAction = $scope.realm.accessCodeLifespanUserAction.toSeconds();
|
||||||
$scope.realm.accessCodeLifespanLogin = $scope.realm.accessCodeLifespanLogin.toSeconds();
|
$scope.realm.accessCodeLifespanLogin = $scope.realm.accessCodeLifespanLogin.toSeconds();
|
||||||
|
|
|
@ -74,6 +74,32 @@
|
||||||
<kc-tooltip>{{:: 'offline-session-idle.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'offline-session-idle.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- KEYCLOAK-7688 Offline Session Max for Offline Token -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-2 control-label" for="offlineSessionMaxLifespanEnabled">{{:: 'offline-session-max-limited' | translate}}</label>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<input ng-change="" ng-model="realm.offlineSessionMaxLifespanEnabled"
|
||||||
|
name="offlineSessionMaxLifespanEnabled"
|
||||||
|
id="offlineSessionMaxLifespanEnabled"
|
||||||
|
onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'offline-session-max-limited.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" data-ng-show="realm.offlineSessionMaxLifespanEnabled">
|
||||||
|
<label class="col-md-2 control-label" for="offlineSessionMaxLifespan">{{:: 'offline-session-max' | translate}}</label>
|
||||||
|
<div class="col-md-6 time-selector">
|
||||||
|
<input class="form-control" type="number" required min="1"
|
||||||
|
max="31536000" data-ng-model="realm.offlineSessionMaxLifespan.time"
|
||||||
|
id="offlineSessionMaxLifespan" name="offlineSessionMaxLifespan"/>
|
||||||
|
<select class="form-control" name="offlineSessionMaxLifespanUnit" data-ng-model="realm.offlineSessionMaxLifespan.unit">
|
||||||
|
<option value="Minutes">{{:: 'minutes' | translate}}</option>
|
||||||
|
<option value="Hours">{{:: 'hours' | translate}}</option>
|
||||||
|
<option value="Days">{{:: 'days' | translate}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'offline-session-max.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-2 control-label" for="accessTokenLifespan">{{:: 'access-token-lifespan' | translate}}</label>
|
<label class="col-md-2 control-label" for="accessTokenLifespan">{{:: 'access-token-lifespan' | translate}}</label>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue