keycloak-uncached
Changes
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java 15(+15 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java 86(+86 -0)
Details
diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
index 200e4e1..2ecbae4 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -44,6 +44,9 @@ public class RealmRepresentation {
protected Integer ssoSessionIdleTimeout;
protected Integer ssoSessionMaxLifespan;
protected Integer offlineSessionIdleTimeout;
+ // KEYCLOAK-7688 Offline Session Max for Offline Token
+ protected Boolean offlineSessionMaxLifespanEnabled;
+ protected Integer offlineSessionMaxLifespan;
protected Integer accessCodeLifespan;
protected Integer accessCodeLifespanUserAction;
protected Integer accessCodeLifespanLogin;
@@ -296,6 +299,23 @@ public class RealmRepresentation {
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() {
return scopeMappings;
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
index 7f9cbed..e16b092 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
@@ -79,6 +79,9 @@ public class CachedRealm extends AbstractExtendableRevisioned {
protected int ssoSessionIdleTimeout;
protected int ssoSessionMaxLifespan;
protected int offlineSessionIdleTimeout;
+ // KEYCLOAK-7688 Offline Session Max for Offline Token
+ protected boolean offlineSessionMaxLifespanEnabled;
+ protected int offlineSessionMaxLifespan;
protected int accessTokenLifespan;
protected int accessTokenLifespanForImplicitFlow;
protected int accessCodeLifespan;
@@ -181,6 +184,9 @@ public class CachedRealm extends AbstractExtendableRevisioned {
ssoSessionIdleTimeout = model.getSsoSessionIdleTimeout();
ssoSessionMaxLifespan = model.getSsoSessionMaxLifespan();
offlineSessionIdleTimeout = model.getOfflineSessionIdleTimeout();
+ // KEYCLOAK-7688 Offline Session Max for Offline Token
+ offlineSessionMaxLifespanEnabled = model.isOfflineSessionMaxLifespanEnabled();
+ offlineSessionMaxLifespan = model.getOfflineSessionMaxLifespan();
accessTokenLifespan = model.getAccessTokenLifespan();
accessTokenLifespanForImplicitFlow = model.getAccessTokenLifespanForImplicitFlow();
accessCodeLifespan = model.getAccessCodeLifespan();
@@ -405,6 +411,15 @@ public class CachedRealm extends AbstractExtendableRevisioned {
return offlineSessionIdleTimeout;
}
+ // KEYCLOAK-7688 Offline Session Max for Offline Token
+ public boolean isOfflineSessionMaxLifespanEnabled() {
+ return offlineSessionMaxLifespanEnabled;
+ }
+
+ public int getOfflineSessionMaxLifespan() {
+ return offlineSessionMaxLifespan;
+ }
+
public int getAccessTokenLifespan() {
return accessTokenLifespan;
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index 9a058b5..386d974 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -418,6 +418,31 @@ public class RealmAdapter implements CachedRealmModel {
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
public int getAccessTokenLifespan() {
if (isUpdated()) return updated.getAccessTokenLifespan();
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributes.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributes.java
index 6ee1074..59f5452 100644
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributes.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributes.java
@@ -30,4 +30,8 @@ public interface RealmAttributes {
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";
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 2795c39..f006180 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -466,6 +466,27 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
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
public int getAccessCodeLifespan() {
return realm.getAccessCodeLifespan();
diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
index 581b59b..3a76c1f 100755
--- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
@@ -179,6 +179,13 @@ public interface RealmModel extends RoleContainerModel {
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);
int getAccessTokenLifespanForImplicitFlow();
diff --git a/server-spi-private/src/main/java/org/keycloak/models/Constants.java b/server-spi-private/src/main/java/org/keycloak/models/Constants.java
index 8a682dc..38e25c7 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/Constants.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/Constants.java
@@ -50,6 +50,9 @@ public interface Constants {
int DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT = 900;
// 30 days
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_CODE = "VERIFY_EMAIL_CODE";
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 3fb7574..24abbe4 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -265,6 +265,9 @@ public class ModelToRepresentation {
rep.setSsoSessionIdleTimeout(realm.getSsoSessionIdleTimeout());
rep.setSsoSessionMaxLifespan(realm.getSsoSessionMaxLifespan());
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.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
rep.setAccessCodeLifespanLogin(realm.getAccessCodeLifespanLogin());
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index f3f57b8..64e4719 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -196,6 +196,14 @@ public class RepresentationToModel {
newRealm.setOfflineSessionIdleTimeout(rep.getOfflineSessionIdleTimeout());
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());
else newRealm.setAccessCodeLifespan(60);
@@ -906,6 +914,10 @@ public class RepresentationToModel {
if (rep.getSsoSessionMaxLifespan() != null) realm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan());
if (rep.getOfflineSessionIdleTimeout() != null)
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) {
realm.updateRequiredCredentials(rep.getRequiredCredentials());
}
diff --git a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java
index 7de8ee4..344203b 100755
--- a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java
+++ b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java
@@ -76,6 +76,9 @@ public class ApplianceBootstrap {
realm.setAccessTokenLifespanForImplicitFlow(Constants.DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT);
realm.setSsoSessionMaxLifespan(36000);
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.setAccessCodeLifespanUserAction(300);
realm.setAccessCodeLifespanLogin(1800);
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 3753bc2..60ae93f 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -118,11 +118,16 @@ public class AuthenticationManager {
return false;
}
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
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) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
index 0b91453..1ffc4ed 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
@@ -30,11 +30,13 @@ import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.RoleResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.constants.ServiceAccountConstants;
+import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.Constants;
import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.models.utils.SessionTimeoutHelper;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken;
@@ -661,4 +663,88 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
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]);
+ }
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java
index 243b648..50cdcba 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java
@@ -225,4 +225,20 @@ public class RealmBuilder {
rep.getGroups().add(group);
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;
+ }
}
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index dd7d013..c13aecd 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -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.
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.
+
+## 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.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
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index eee2f44..44a74a5 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -1090,6 +1090,8 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
$scope.realm.ssoSessionIdleTimeout = TimeUnit2.asUnit(realm.ssoSessionIdleTimeout);
$scope.realm.ssoSessionMaxLifespan = TimeUnit2.asUnit(realm.ssoSessionMaxLifespan);
$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.accessCodeLifespanLogin = TimeUnit2.asUnit(realm.accessCodeLifespanLogin);
$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.ssoSessionMaxLifespan = $scope.realm.ssoSessionMaxLifespan.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.accessCodeLifespanUserAction = $scope.realm.accessCodeLifespanUserAction.toSeconds();
$scope.realm.accessCodeLifespanLogin = $scope.realm.accessCodeLifespanLogin.toSeconds();
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
index 3b05fdb..3887583 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
@@ -74,6 +74,32 @@
<kc-tooltip>{{:: 'offline-session-idle.tooltip' | translate}}</kc-tooltip>
</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">
<label class="col-md-2 control-label" for="accessTokenLifespan">{{:: 'access-token-lifespan' | translate}}</label>