KEYCLOAK-1267 Add dedicated SSO timeouts for Remember-Me
Previously remember-me sessions where tied to the SSO max session timeout which could lead to unexpected early session timeouts. We now allow SSO timeouts to be configured separately for sessions with enabled remember-me. This enables users to opt-in for longer session timeouts. SSO session timeouts for remember-me can now be configured in the tokens tab in the realm admin console. This new configuration is optional and will tipically host values larger than the regular max SSO timeouts. If no value is specified for remember-me timeouts then the regular max SSO timeouts will be used. Work based on PR https://github.com/keycloak/keycloak/pull/3161 by Thomas Darimont <thomas.darimont@gmail.com>
This commit is contained in:
parent
8c650f9f6a
commit
cf57a1bc4b
25 changed files with 435 additions and 15 deletions
|
@ -44,6 +44,8 @@ public class RealmRepresentation {
|
|||
protected Integer accessTokenLifespanForImplicitFlow;
|
||||
protected Integer ssoSessionIdleTimeout;
|
||||
protected Integer ssoSessionMaxLifespan;
|
||||
protected Integer ssoSessionIdleTimeoutRememberMe;
|
||||
protected Integer ssoSessionMaxLifespanRememberMe;
|
||||
protected Integer offlineSessionIdleTimeout;
|
||||
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||
protected Boolean offlineSessionMaxLifespanEnabled;
|
||||
|
@ -300,6 +302,22 @@ public class RealmRepresentation {
|
|||
this.ssoSessionMaxLifespan = ssoSessionMaxLifespan;
|
||||
}
|
||||
|
||||
public Integer getSsoSessionMaxLifespanRememberMe() {
|
||||
return ssoSessionMaxLifespanRememberMe;
|
||||
}
|
||||
|
||||
public void setSsoSessionMaxLifespanRememberMe(Integer ssoSessionMaxLifespanRememberMe) {
|
||||
this.ssoSessionMaxLifespanRememberMe = ssoSessionMaxLifespanRememberMe;
|
||||
}
|
||||
|
||||
public Integer getSsoSessionIdleTimeoutRememberMe() {
|
||||
return ssoSessionIdleTimeoutRememberMe;
|
||||
}
|
||||
|
||||
public void setSsoSessionIdleTimeoutRememberMe(Integer ssoSessionIdleTimeoutRememberMe) {
|
||||
this.ssoSessionIdleTimeoutRememberMe = ssoSessionIdleTimeoutRememberMe;
|
||||
}
|
||||
|
||||
public Integer getOfflineSessionIdleTimeout() {
|
||||
return offlineSessionIdleTimeout;
|
||||
}
|
||||
|
|
|
@ -417,6 +417,30 @@ public class RealmAdapter implements CachedRealmModel {
|
|||
updated.setSsoSessionMaxLifespan(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSsoSessionIdleTimeoutRememberMe() {
|
||||
if (updated != null) return updated.getSsoSessionIdleTimeoutRememberMe();
|
||||
return cached.getSsoSessionIdleTimeoutRememberMe();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSsoSessionIdleTimeoutRememberMe(int seconds) {
|
||||
getDelegateForUpdate();
|
||||
updated.setSsoSessionIdleTimeoutRememberMe(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSsoSessionMaxLifespanRememberMe() {
|
||||
if (updated != null) return updated.getSsoSessionMaxLifespanRememberMe();
|
||||
return cached.getSsoSessionMaxLifespanRememberMe();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSsoSessionMaxLifespanRememberMe(int seconds) {
|
||||
getDelegateForUpdate();
|
||||
updated.setSsoSessionMaxLifespanRememberMe(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOfflineSessionIdleTimeout() {
|
||||
if (isUpdated()) return updated.getOfflineSessionIdleTimeout();
|
||||
|
|
|
@ -79,6 +79,8 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
|||
protected int refreshTokenMaxReuse;
|
||||
protected int ssoSessionIdleTimeout;
|
||||
protected int ssoSessionMaxLifespan;
|
||||
protected int ssoSessionIdleTimeoutRememberMe;
|
||||
protected int ssoSessionMaxLifespanRememberMe;
|
||||
protected int offlineSessionIdleTimeout;
|
||||
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||
protected boolean offlineSessionMaxLifespanEnabled;
|
||||
|
@ -185,6 +187,8 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
|||
refreshTokenMaxReuse = model.getRefreshTokenMaxReuse();
|
||||
ssoSessionIdleTimeout = model.getSsoSessionIdleTimeout();
|
||||
ssoSessionMaxLifespan = model.getSsoSessionMaxLifespan();
|
||||
ssoSessionIdleTimeoutRememberMe = model.getSsoSessionIdleTimeoutRememberMe();
|
||||
ssoSessionMaxLifespanRememberMe = model.getSsoSessionMaxLifespanRememberMe();
|
||||
offlineSessionIdleTimeout = model.getOfflineSessionIdleTimeout();
|
||||
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||
offlineSessionMaxLifespanEnabled = model.isOfflineSessionMaxLifespanEnabled();
|
||||
|
@ -413,6 +417,14 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
|||
return ssoSessionMaxLifespan;
|
||||
}
|
||||
|
||||
public int getSsoSessionIdleTimeoutRememberMe() {
|
||||
return ssoSessionIdleTimeoutRememberMe;
|
||||
}
|
||||
|
||||
public int getSsoSessionMaxLifespanRememberMe() {
|
||||
return ssoSessionMaxLifespanRememberMe;
|
||||
}
|
||||
|
||||
public int getOfflineSessionIdleTimeout() {
|
||||
return offlineSessionIdleTimeout;
|
||||
}
|
||||
|
|
|
@ -470,6 +470,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
private void removeExpiredUserSessions(RealmModel realm) {
|
||||
int expired = Time.currentTime() - realm.getSsoSessionMaxLifespan();
|
||||
int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout() - SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
|
||||
int expiredRememberMe = Time.currentTime() - (realm.getSsoSessionMaxLifespanRememberMe() > 0 ? realm.getSsoSessionMaxLifespanRememberMe() : realm.getSsoSessionMaxLifespan());
|
||||
int expiredRefreshRememberMe = Time.currentTime() - (realm.getSsoSessionIdleTimeoutRememberMe() > 0 ? realm.getSsoSessionIdleTimeoutRememberMe() : realm.getSsoSessionIdleTimeout()) -
|
||||
SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
|
||||
|
||||
FuturesHelper futures = new FuturesHelper();
|
||||
|
||||
|
@ -484,7 +487,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
localCacheStoreIgnore
|
||||
.entrySet()
|
||||
.stream()
|
||||
.filter(UserSessionPredicate.create(realm.getId()).expired(expired, expiredRefresh))
|
||||
.filter(UserSessionPredicate.create(realm.getId()).expired(expired, expiredRefresh, expiredRememberMe, expiredRefreshRememberMe))
|
||||
.map(Mappers.userSessionEntity())
|
||||
.forEach(new Consumer<UserSessionEntity>() {
|
||||
|
||||
|
|
|
@ -47,6 +47,10 @@ public class UserSessionPredicate implements Predicate<Map.Entry<String, Session
|
|||
|
||||
private Integer expiredRefresh;
|
||||
|
||||
private Integer expiredRememberMe;
|
||||
|
||||
private Integer expiredRefreshRememberMe;
|
||||
|
||||
private String brokerSessionId;
|
||||
private String brokerUserId;
|
||||
|
||||
|
@ -82,11 +86,18 @@ public class UserSessionPredicate implements Predicate<Map.Entry<String, Session
|
|||
}
|
||||
|
||||
public UserSessionPredicate expired(Integer expired, Integer expiredRefresh) {
|
||||
return this.expired(expired, expiredRefresh, null, null);
|
||||
}
|
||||
|
||||
public UserSessionPredicate expired(Integer expired, Integer expiredRefresh, Integer expiredRememberMe, Integer expiredRefreshRememberMe) {
|
||||
this.expired = expired;
|
||||
this.expiredRefresh = expiredRefresh;
|
||||
this.expiredRememberMe = expiredRememberMe;
|
||||
this.expiredRefreshRememberMe = expiredRefreshRememberMe;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public UserSessionPredicate brokerSessionId(String id) {
|
||||
this.brokerSessionId = id;
|
||||
return this;
|
||||
|
@ -121,14 +132,20 @@ public class UserSessionPredicate implements Predicate<Map.Entry<String, Session
|
|||
return false;
|
||||
}
|
||||
|
||||
if (expired != null && expiredRefresh != null && entity.getStarted() > expired && entity.getLastSessionRefresh() > expiredRefresh) {
|
||||
return false;
|
||||
if (entity.isRememberMe()) {
|
||||
if (expiredRememberMe != null && expiredRefreshRememberMe != null && entity.getStarted() > expiredRefreshRememberMe && entity.getLastSessionRefresh() > expiredRefreshRememberMe) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (expired != null && expiredRefresh != null && entity.getStarted() > expired && entity.getLastSessionRefresh() > expiredRefresh) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (expired == null && expiredRefresh != null && entity.getLastSessionRefresh() > expiredRefresh) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -466,6 +466,26 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
realm.setSsoSessionMaxLifespan(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSsoSessionIdleTimeoutRememberMe() {
|
||||
return realm.getSsoSessionIdleTimeoutRememberMe();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSsoSessionIdleTimeoutRememberMe(int seconds){
|
||||
realm.setSsoSessionIdleTimeoutRememberMe(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSsoSessionMaxLifespanRememberMe() {
|
||||
return realm.getSsoSessionMaxLifespanRememberMe();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSsoSessionMaxLifespanRememberMe(int seconds) {
|
||||
realm.setSsoSessionMaxLifespanRememberMe(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOfflineSessionIdleTimeout() {
|
||||
return realm.getOfflineSessionIdleTimeout();
|
||||
|
|
|
@ -109,6 +109,10 @@ public class RealmEntity {
|
|||
private int ssoSessionIdleTimeout;
|
||||
@Column(name="SSO_MAX_LIFESPAN")
|
||||
private int ssoSessionMaxLifespan;
|
||||
@Column(name="SSO_IDLE_TIMEOUT_REMEMBER_ME")
|
||||
private int ssoSessionIdleTimeoutRememberMe;
|
||||
@Column(name="SSO_MAX_LIFESPAN_REMEMBER_ME")
|
||||
private int ssoSessionMaxLifespanRememberMe;
|
||||
@Column(name="OFFLINE_SESSION_IDLE_TIMEOUT")
|
||||
private int offlineSessionIdleTimeout;
|
||||
@Column(name="ACCESS_TOKEN_LIFESPAN")
|
||||
|
@ -370,6 +374,22 @@ public class RealmEntity {
|
|||
this.ssoSessionMaxLifespan = ssoSessionMaxLifespan;
|
||||
}
|
||||
|
||||
public int getSsoSessionIdleTimeoutRememberMe() {
|
||||
return ssoSessionIdleTimeoutRememberMe;
|
||||
}
|
||||
|
||||
public void setSsoSessionIdleTimeoutRememberMe(int ssoSessionIdleTimeoutRememberMe) {
|
||||
this.ssoSessionIdleTimeoutRememberMe = ssoSessionIdleTimeoutRememberMe;
|
||||
}
|
||||
|
||||
public int getSsoSessionMaxLifespanRememberMe() {
|
||||
return ssoSessionMaxLifespanRememberMe;
|
||||
}
|
||||
|
||||
public void setSsoSessionMaxLifespanRememberMe(int ssoSessionMaxLifespanRememberMe) {
|
||||
this.ssoSessionMaxLifespanRememberMe = ssoSessionMaxLifespanRememberMe;
|
||||
}
|
||||
|
||||
public int getOfflineSessionIdleTimeout() {
|
||||
return offlineSessionIdleTimeout;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!--
|
||||
~ * Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||
~ * and other contributors as indicated by the @author tags.
|
||||
~ *
|
||||
~ * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ * you may not use this file except in compliance with the License.
|
||||
~ * You may obtain a copy of the License at
|
||||
~ *
|
||||
~ * http://www.apache.org/licenses/LICENSE-2.0
|
||||
~ *
|
||||
~ * Unless required by applicable law or agreed to in writing, software
|
||||
~ * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ * See the License for the specific language governing permissions and
|
||||
~ * limitations under the License.
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||
|
||||
<changeSet author="sguilhen@redhat.com" id="4.7.0-KEYCLOAK-1267">
|
||||
<addColumn tableName="REALM">
|
||||
<column name="SSO_MAX_LIFESPAN_REMEMBER_ME" type="INT"/>
|
||||
<column name="SSO_IDLE_TIMEOUT_REMEMBER_ME" type="INT"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
|
@ -60,4 +60,5 @@
|
|||
<include file="META-INF/jpa-changelog-4.2.0.xml"/>
|
||||
<include file="META-INF/jpa-changelog-4.3.0.xml"/>
|
||||
<include file="META-INF/jpa-changelog-4.6.0.xml"/>
|
||||
<include file="META-INF/jpa-changelog-4.7.0.xml"/>
|
||||
</databaseChangeLog>
|
||||
|
|
|
@ -293,6 +293,8 @@ public class ModelToRepresentation {
|
|||
rep.setAccessTokenLifespanForImplicitFlow(realm.getAccessTokenLifespanForImplicitFlow());
|
||||
rep.setSsoSessionIdleTimeout(realm.getSsoSessionIdleTimeout());
|
||||
rep.setSsoSessionMaxLifespan(realm.getSsoSessionMaxLifespan());
|
||||
rep.setSsoSessionIdleTimeoutRememberMe(realm.getSsoSessionIdleTimeoutRememberMe());
|
||||
rep.setSsoSessionMaxLifespanRememberMe(realm.getSsoSessionMaxLifespanRememberMe());
|
||||
rep.setOfflineSessionIdleTimeout(realm.getOfflineSessionIdleTimeout());
|
||||
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||
rep.setOfflineSessionMaxLifespanEnabled(realm.isOfflineSessionMaxLifespanEnabled());
|
||||
|
|
|
@ -195,6 +195,8 @@ public class RepresentationToModel {
|
|||
else newRealm.setSsoSessionIdleTimeout(1800);
|
||||
if (rep.getSsoSessionMaxLifespan() != null) newRealm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan());
|
||||
else newRealm.setSsoSessionMaxLifespan(36000);
|
||||
if (rep.getSsoSessionMaxLifespanRememberMe() != null) newRealm.setSsoSessionMaxLifespanRememberMe(rep.getSsoSessionMaxLifespanRememberMe());
|
||||
if (rep.getSsoSessionIdleTimeoutRememberMe() != null) newRealm.setSsoSessionIdleTimeoutRememberMe(rep.getSsoSessionIdleTimeoutRememberMe());
|
||||
if (rep.getOfflineSessionIdleTimeout() != null)
|
||||
newRealm.setOfflineSessionIdleTimeout(rep.getOfflineSessionIdleTimeout());
|
||||
else newRealm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT);
|
||||
|
@ -916,6 +918,8 @@ public class RepresentationToModel {
|
|||
realm.setAccessTokenLifespanForImplicitFlow(rep.getAccessTokenLifespanForImplicitFlow());
|
||||
if (rep.getSsoSessionIdleTimeout() != null) realm.setSsoSessionIdleTimeout(rep.getSsoSessionIdleTimeout());
|
||||
if (rep.getSsoSessionMaxLifespan() != null) realm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan());
|
||||
if (rep.getSsoSessionIdleTimeoutRememberMe() != null) realm.setSsoSessionIdleTimeoutRememberMe(rep.getSsoSessionIdleTimeoutRememberMe());
|
||||
if (rep.getSsoSessionMaxLifespanRememberMe() != null) realm.setSsoSessionMaxLifespanRememberMe(rep.getSsoSessionMaxLifespanRememberMe());
|
||||
if (rep.getOfflineSessionIdleTimeout() != null)
|
||||
realm.setOfflineSessionIdleTimeout(rep.getOfflineSessionIdleTimeout());
|
||||
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||
|
|
|
@ -177,6 +177,12 @@ public interface RealmModel extends RoleContainerModel {
|
|||
int getSsoSessionMaxLifespan();
|
||||
void setSsoSessionMaxLifespan(int seconds);
|
||||
|
||||
int getSsoSessionIdleTimeoutRememberMe();
|
||||
void setSsoSessionIdleTimeoutRememberMe(int seconds);
|
||||
|
||||
int getSsoSessionMaxLifespanRememberMe();
|
||||
void setSsoSessionMaxLifespanRememberMe(int seconds);
|
||||
|
||||
int getOfflineSessionIdleTimeout();
|
||||
void setOfflineSessionIdleTimeout(int seconds);
|
||||
|
||||
|
|
|
@ -72,7 +72,8 @@ public class SessionsBean {
|
|||
}
|
||||
|
||||
public Date getExpires() {
|
||||
int max = session.getStarted() + realm.getSsoSessionMaxLifespan();
|
||||
int maxLifespan = session.isRememberMe() && realm.getSsoSessionMaxLifespanRememberMe() > 0 ? realm.getSsoSessionMaxLifespanRememberMe() : realm.getSsoSessionMaxLifespan();
|
||||
int max = session.getStarted() + maxLifespan;
|
||||
return Time.toDate(max);
|
||||
}
|
||||
|
||||
|
|
|
@ -656,13 +656,15 @@ public class TokenManager {
|
|||
|
||||
int expiration;
|
||||
if (tokenLifespan == -1) {
|
||||
expiration = userSession.getStarted() + realm.getSsoSessionMaxLifespan();
|
||||
expiration = userSession.getStarted() + (userSession.isRememberMe() && realm.getSsoSessionMaxLifespanRememberMe() > 0 ?
|
||||
realm.getSsoSessionMaxLifespanRememberMe() : realm.getSsoSessionMaxLifespan());
|
||||
} else {
|
||||
expiration = Time.currentTime() + tokenLifespan;
|
||||
}
|
||||
|
||||
if (!userSession.isOffline()) {
|
||||
int sessionExpires = userSession.getStarted() + realm.getSsoSessionMaxLifespan();
|
||||
int sessionExpires = userSession.getStarted() + (userSession.isRememberMe() && realm.getSsoSessionMaxLifespanRememberMe() > 0 ?
|
||||
realm.getSsoSessionMaxLifespanRememberMe() : realm.getSsoSessionMaxLifespan());
|
||||
expiration = expiration <= sessionExpires ? expiration : sessionExpires;
|
||||
}
|
||||
|
||||
|
@ -756,8 +758,10 @@ public class TokenManager {
|
|||
}
|
||||
|
||||
private int getRefreshExpiration() {
|
||||
int sessionExpires = userSession.getStarted() + realm.getSsoSessionMaxLifespan();
|
||||
int expiration = Time.currentTime() + realm.getSsoSessionIdleTimeout();
|
||||
int sessionExpires = userSession.getStarted() + (userSession.isRememberMe() && realm.getSsoSessionMaxLifespanRememberMe() > 0 ?
|
||||
realm.getSsoSessionMaxLifespanRememberMe() : realm.getSsoSessionMaxLifespan());
|
||||
int expiration = Time.currentTime() + (userSession.isRememberMe() && realm.getSsoSessionIdleTimeoutRememberMe() > 0 ?
|
||||
realm.getSsoSessionIdleTimeoutRememberMe() : realm.getSsoSessionIdleTimeout());
|
||||
return expiration <= sessionExpires ? expiration : sessionExpires;
|
||||
}
|
||||
|
||||
|
|
|
@ -130,12 +130,16 @@ public class AuthenticationManager {
|
|||
return false;
|
||||
}
|
||||
int currentTime = Time.currentTime();
|
||||
int max = userSession.getStarted() + realm.getSsoSessionMaxLifespan();
|
||||
|
||||
// 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.getSsoSessionIdleTimeout() + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS;
|
||||
int maxIdle = (userSession.isRememberMe() && realm.getSsoSessionIdleTimeoutRememberMe() > 0 ?
|
||||
realm.getSsoSessionIdleTimeoutRememberMe() : realm.getSsoSessionIdleTimeout()) + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS;
|
||||
int maxLifespan = userSession.isRememberMe() && realm.getSsoSessionMaxLifespanRememberMe() > 0 ?
|
||||
realm.getSsoSessionMaxLifespanRememberMe() : realm.getSsoSessionMaxLifespan();
|
||||
|
||||
return userSession.getLastSessionRefresh() + maxIdle > currentTime && max > currentTime;
|
||||
boolean sessionMaxOk = userSession.getStarted() + maxLifespan > currentTime;
|
||||
boolean sessionIdleOk = userSession.getLastSessionRefresh() + maxIdle > currentTime;
|
||||
return sessionIdleOk && sessionMaxOk;
|
||||
}
|
||||
|
||||
public static boolean isOfflineSessionValid(RealmModel realm, UserSessionModel userSession) {
|
||||
|
@ -576,10 +580,14 @@ public class AuthenticationManager {
|
|||
token.issuedNow();
|
||||
token.subject(user.getId());
|
||||
token.issuer(issuer);
|
||||
|
||||
if (session != null) {
|
||||
token.setSessionState(session.getId());
|
||||
}
|
||||
if (realm.getSsoSessionMaxLifespan() > 0) {
|
||||
|
||||
if (session != null && session.isRememberMe() && realm.getSsoSessionMaxLifespanRememberMe() > 0) {
|
||||
token.expiration(Time.currentTime() + realm.getSsoSessionMaxLifespanRememberMe());
|
||||
} else if (realm.getSsoSessionMaxLifespan() > 0) {
|
||||
token.expiration(Time.currentTime() + realm.getSsoSessionMaxLifespan());
|
||||
}
|
||||
|
||||
|
@ -601,7 +609,7 @@ public class AuthenticationManager {
|
|||
boolean secureOnly = realm.getSslRequired().isRequired(connection);
|
||||
int maxAge = NewCookie.DEFAULT_MAX_AGE;
|
||||
if (session != null && session.isRememberMe()) {
|
||||
maxAge = realm.getSsoSessionMaxLifespan();
|
||||
maxAge = realm.getSsoSessionMaxLifespanRememberMe() > 0 ? realm.getSsoSessionMaxLifespanRememberMe() : realm.getSsoSessionMaxLifespan();
|
||||
}
|
||||
logger.debugv("Create login cookie - name: {0}, path: {1}, max-age: {2}", KEYCLOAK_IDENTITY_COOKIE, cookiePath, maxAge);
|
||||
CookieHelper.addCookie(KEYCLOAK_IDENTITY_COOKIE, encoded, cookiePath, null, null, maxAge, secureOnly, true);
|
||||
|
@ -613,7 +621,8 @@ public class AuthenticationManager {
|
|||
}
|
||||
// THIS SHOULD NOT BE A HTTPONLY COOKIE! It is used for OpenID Connect Iframe Session support!
|
||||
// Max age should be set to the max lifespan of the session as it's used to invalidate old-sessions on re-login
|
||||
CookieHelper.addCookie(KEYCLOAK_SESSION_COOKIE, sessionCookieValue, cookiePath, null, null, realm.getSsoSessionMaxLifespan(), secureOnly, false);
|
||||
int sessionCookieMaxAge = session.isRememberMe() && realm.getSsoSessionMaxLifespanRememberMe() > 0 ? realm.getSsoSessionMaxLifespanRememberMe() : realm.getSsoSessionMaxLifespan();
|
||||
CookieHelper.addCookie(KEYCLOAK_SESSION_COOKIE, sessionCookieValue, cookiePath, null, null, sessionCookieMaxAge, secureOnly, false);
|
||||
P3PHelper.addP3PHeader(keycloakSession);
|
||||
}
|
||||
|
||||
|
|
|
@ -227,12 +227,26 @@ public class OAuthClient {
|
|||
return doLogin(user.getUsername(), getPasswordOf(user));
|
||||
}
|
||||
|
||||
public AuthorizationEndpointResponse doRememberMeLogin(String username, String password) {
|
||||
openLoginForm();
|
||||
fillLoginForm(username, password, true);
|
||||
|
||||
return new AuthorizationEndpointResponse(this);
|
||||
}
|
||||
|
||||
public void fillLoginForm(String username, String password) {
|
||||
this.fillLoginForm(username, password, false);
|
||||
}
|
||||
|
||||
public void fillLoginForm(String username, String password, boolean rememberMe) {
|
||||
WaitUtils.waitForPageToLoad();
|
||||
String src = driver.getPageSource();
|
||||
try {
|
||||
driver.findElement(By.id("username")).sendKeys(username);
|
||||
driver.findElement(By.id("password")).sendKeys(password);
|
||||
if (rememberMe) {
|
||||
driver.findElement(By.id("rememberMe")).click();
|
||||
}
|
||||
driver.findElement(By.name("login")).click();
|
||||
} catch (Throwable t) {
|
||||
System.err.println(src);
|
||||
|
|
|
@ -364,6 +364,8 @@ public class RealmTest extends AbstractAdminTest {
|
|||
RealmRepresentation rep = realm.toRepresentation();
|
||||
rep.setSsoSessionIdleTimeout(123);
|
||||
rep.setSsoSessionMaxLifespan(12);
|
||||
rep.setSsoSessionIdleTimeoutRememberMe(33);
|
||||
rep.setSsoSessionMaxLifespanRememberMe(34);
|
||||
rep.setAccessCodeLifespanLogin(1234);
|
||||
rep.setActionTokenGeneratedByAdminLifespan(2345);
|
||||
rep.setActionTokenGeneratedByUserLifespan(3456);
|
||||
|
@ -379,6 +381,8 @@ public class RealmTest extends AbstractAdminTest {
|
|||
|
||||
assertEquals(123, rep.getSsoSessionIdleTimeout().intValue());
|
||||
assertEquals(12, rep.getSsoSessionMaxLifespan().intValue());
|
||||
assertEquals(33, rep.getSsoSessionIdleTimeoutRememberMe().intValue());
|
||||
assertEquals(34, rep.getSsoSessionMaxLifespanRememberMe().intValue());
|
||||
assertEquals(1234, rep.getAccessCodeLifespanLogin().intValue());
|
||||
assertEquals(2345, rep.getActionTokenGeneratedByAdminLifespan().intValue());
|
||||
assertEquals(3456, rep.getActionTokenGeneratedByUserLifespan().intValue());
|
||||
|
@ -571,6 +575,8 @@ public class RealmTest extends AbstractAdminTest {
|
|||
if (realm.getAccessTokenLifespanForImplicitFlow() != null) assertEquals(realm.getAccessTokenLifespanForImplicitFlow(), storedRealm.getAccessTokenLifespanForImplicitFlow());
|
||||
if (realm.getSsoSessionIdleTimeout() != null) assertEquals(realm.getSsoSessionIdleTimeout(), storedRealm.getSsoSessionIdleTimeout());
|
||||
if (realm.getSsoSessionMaxLifespan() != null) assertEquals(realm.getSsoSessionMaxLifespan(), storedRealm.getSsoSessionMaxLifespan());
|
||||
if (realm.getSsoSessionIdleTimeoutRememberMe() != null) Assert.assertEquals(realm.getSsoSessionIdleTimeoutRememberMe(), storedRealm.getSsoSessionIdleTimeoutRememberMe());
|
||||
if (realm.getSsoSessionMaxLifespanRememberMe() != null) Assert.assertEquals(realm.getSsoSessionMaxLifespanRememberMe(), storedRealm.getSsoSessionMaxLifespanRememberMe());
|
||||
if (realm.getRequiredCredentials() != null) {
|
||||
assertNotNull(storedRealm.getRequiredCredentials());
|
||||
for (String cred : realm.getRequiredCredentials()) {
|
||||
|
|
|
@ -82,6 +82,10 @@ public class ExportImportUtil {
|
|||
Assert.assertTrue(realm.isVerifyEmail());
|
||||
Assert.assertEquals((Integer)3600000, realm.getOfflineSessionIdleTimeout());
|
||||
Assert.assertEquals((Integer)1500, realm.getAccessTokenLifespanForImplicitFlow());
|
||||
Assert.assertEquals((Integer)1800, realm.getSsoSessionIdleTimeout());
|
||||
Assert.assertEquals((Integer)36000, realm.getSsoSessionMaxLifespan());
|
||||
Assert.assertEquals((Integer)3600, realm.getSsoSessionIdleTimeoutRememberMe());
|
||||
Assert.assertEquals((Integer)172800, realm.getSsoSessionMaxLifespanRememberMe());
|
||||
|
||||
Set<String> creds = realm.getRequiredCredentials();
|
||||
Assert.assertEquals(1, creds.size());
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.keycloak.jose.jws.JWSInput;
|
|||
import org.keycloak.jose.jws.JWSInputException;
|
||||
import org.keycloak.models.BrowserSecurityHeaders;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.utils.SessionTimeoutHelper;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
|
@ -526,8 +527,14 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
|
|||
}
|
||||
|
||||
private void setRememberMe(boolean enabled) {
|
||||
this.setRememberMe(enabled, null, null);
|
||||
}
|
||||
|
||||
private void setRememberMe(boolean enabled, Integer idleTimeout, Integer maxLifespan) {
|
||||
RealmRepresentation rep = adminClient.realm("test").toRepresentation();
|
||||
rep.setRememberMe(enabled);
|
||||
rep.setSsoSessionIdleTimeoutRememberMe(idleTimeout);
|
||||
rep.setSsoSessionMaxLifespanRememberMe(maxLifespan);
|
||||
adminClient.realm("test").update(rep);
|
||||
}
|
||||
|
||||
|
@ -749,4 +756,62 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
|
|||
events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginRememberMeExpiredIdle() throws Exception {
|
||||
setRememberMe(true, 1, null);
|
||||
|
||||
try {
|
||||
// login form shown after redirect from app
|
||||
oauth.clientId("test-app");
|
||||
oauth.redirectUri(OAuthClient.APP_ROOT + "/auth");
|
||||
oauth.openLoginForm();
|
||||
|
||||
assertTrue(loginPage.isCurrent());
|
||||
loginPage.setRememberMe(true);
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
|
||||
// sucessful login - app page should be on display.
|
||||
events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent();
|
||||
appPage.assertCurrent();
|
||||
|
||||
// expire idle timeout using the timeout window.
|
||||
setTimeOffset(2 + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS);
|
||||
|
||||
// trying to open the account page with an expired idle timeout should redirect back to the login page.
|
||||
appPage.openAccount();
|
||||
loginPage.assertCurrent();
|
||||
} finally {
|
||||
setRememberMe(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginRememberMeExpiredMaxLifespan() throws Exception {
|
||||
setRememberMe(true, null, 1);
|
||||
|
||||
try {
|
||||
// login form shown after redirect from app
|
||||
oauth.clientId("test-app");
|
||||
oauth.redirectUri(OAuthClient.APP_ROOT + "/auth");
|
||||
oauth.openLoginForm();
|
||||
|
||||
assertTrue(loginPage.isCurrent());
|
||||
loginPage.setRememberMe(true);
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
|
||||
// sucessful login - app page should be on display.
|
||||
events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent();
|
||||
appPage.assertCurrent();
|
||||
|
||||
// expire the max lifespan.
|
||||
setTimeOffset(2);
|
||||
|
||||
// trying to open the account page with an expired lifespan should redirect back to the login page.
|
||||
appPage.openAccount();
|
||||
loginPage.assertCurrent();
|
||||
} finally {
|
||||
setRememberMe(false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -592,6 +592,64 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
|||
setTimeOffset(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserSessionRefreshAndIdleRememberMe() throws Exception {
|
||||
RealmResource testRealm = adminClient.realm("test");
|
||||
RealmRepresentation testRealmRep = testRealm.toRepresentation();
|
||||
Boolean previousRememberMe = testRealmRep.isRememberMe();
|
||||
int originalIdleRememberMe = testRealmRep.getSsoSessionIdleTimeoutRememberMe();
|
||||
|
||||
try {
|
||||
testRealmRep.setRememberMe(true);
|
||||
testRealm.update(testRealmRep);
|
||||
|
||||
oauth.doRememberMeLogin("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");
|
||||
|
||||
events.poll();
|
||||
|
||||
String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
|
||||
int last = testingClient.testing().getLastSessionRefresh("test", sessionId, false);
|
||||
|
||||
setTimeOffset(2);
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||
oauth.verifyToken(tokenResponse.getAccessToken());
|
||||
oauth.parseRefreshToken(tokenResponse.getRefreshToken());
|
||||
assertEquals(200, tokenResponse.getStatusCode());
|
||||
|
||||
int next = testingClient.testing().getLastSessionRefresh("test", sessionId, false);
|
||||
Assert.assertNotEquals(last, next);
|
||||
|
||||
testRealmRep.setSsoSessionIdleTimeoutRememberMe(1);
|
||||
testRealm.update(testRealmRep);
|
||||
|
||||
events.clear();
|
||||
// Needs to add some additional time due the tollerance allowed by IDLE_TIMEOUT_WINDOW_SECONDS
|
||||
setTimeOffset(6 + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS);
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||
|
||||
// test idle remember me timeout
|
||||
assertEquals(400, tokenResponse.getStatusCode());
|
||||
assertNull(tokenResponse.getAccessToken());
|
||||
assertNull(tokenResponse.getRefreshToken());
|
||||
|
||||
events.expectRefresh(refreshId, sessionId).error(Errors.INVALID_TOKEN);
|
||||
events.clear();
|
||||
|
||||
} finally {
|
||||
testRealmRep.setSsoSessionIdleTimeoutRememberMe(originalIdleRememberMe);
|
||||
testRealmRep.setRememberMe(previousRememberMe);
|
||||
testRealm.update(testRealmRep);
|
||||
setTimeOffset(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refreshTokenUserSessionMaxLifespan() throws Exception {
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
|
@ -628,6 +686,57 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
|||
setTimeOffset(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* KEYCLOAK-1267
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void refreshTokenUserSessionMaxLifespanWithRememberMe() throws Exception {
|
||||
|
||||
RealmResource testRealm = adminClient.realm("test");
|
||||
RealmRepresentation testRealmRep = testRealm.toRepresentation();
|
||||
Boolean previousRememberMe = testRealmRep.isRememberMe();
|
||||
int previousSsoMaxLifespanRememberMe = testRealmRep.getSsoSessionMaxLifespanRememberMe();
|
||||
|
||||
try {
|
||||
testRealmRep.setRememberMe(true);
|
||||
testRealm.update(testRealmRep);
|
||||
|
||||
oauth.doRememberMeLogin("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");
|
||||
|
||||
events.poll();
|
||||
|
||||
String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
|
||||
|
||||
testRealmRep.setSsoSessionMaxLifespanRememberMe(1);
|
||||
testRealm.update(testRealmRep);
|
||||
|
||||
setTimeOffset(2);
|
||||
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||
|
||||
assertEquals(400, tokenResponse.getStatusCode());
|
||||
assertNull(tokenResponse.getAccessToken());
|
||||
assertNull(tokenResponse.getRefreshToken());
|
||||
|
||||
events.expectRefresh(refreshId, sessionId).error(Errors.INVALID_TOKEN);
|
||||
events.clear();
|
||||
|
||||
} finally {
|
||||
testRealmRep.setSsoSessionMaxLifespanRememberMe(previousSsoMaxLifespanRememberMe);
|
||||
testRealmRep.setRememberMe(previousRememberMe);
|
||||
testRealm.update(testRealmRep);
|
||||
setTimeOffset(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckSsl() throws Exception {
|
||||
Client client = ClientBuilder.newClient();
|
||||
|
|
|
@ -198,6 +198,16 @@ public class RealmBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public RealmBuilder ssoSessionIdleTimeoutRememberMe(int ssoSessionIdleTimeoutRememberMe){
|
||||
rep.setSsoSessionIdleTimeoutRememberMe(ssoSessionIdleTimeoutRememberMe);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmBuilder ssoSessionMaxLifespanRememberMe(int ssoSessionMaxLifespanRememberMe){
|
||||
rep.setSsoSessionMaxLifespanRememberMe(ssoSessionMaxLifespanRememberMe);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmBuilder accessCodeLifespanUserAction(int accessCodeLifespanUserAction) {
|
||||
rep.setAccessCodeLifespanUserAction(accessCodeLifespanUserAction);
|
||||
return this;
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
"enabled": true,
|
||||
"accessTokenLifespan": 6000,
|
||||
"accessTokenLifespanForImplicitFlow": 1500,
|
||||
"ssoSessionIdleTimeout": 1800,
|
||||
"ssoSessionMaxLifespan": 36000,
|
||||
"ssoSessionIdleTimeoutRememberMe": 3600,
|
||||
"ssoSessionMaxLifespanRememberMe": 172800,
|
||||
"accessCodeLifespan": 30,
|
||||
"accessCodeLifespanUserAction": 600,
|
||||
"offlineSessionIdleTimeout": 3600000,
|
||||
|
|
|
@ -109,6 +109,10 @@ days=Days
|
|||
sso-session-max=SSO Session Max
|
||||
sso-session-idle.tooltip=Time a session is allowed to be idle before it expires. 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.
|
||||
sso-session-idle-remember-me=SSO Session Idle Remember Me
|
||||
sso-session-idle-remember-me.tooltip=Time a remember me session is allowed to be idle before it expires. Tokens and browser sessions are invalidated when a session is expired. If not set it uses the standard SSO Session Idle value.
|
||||
sso-session-max-remember-me=SSO Session Max Remember Me
|
||||
sso-session-max-remember-me.tooltip=Max time before a session is expired when the user has set the remember me option. Tokens and browser sessions are invalidated when a session is expired. If not set it uses the standard SSO Session Max value.
|
||||
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.
|
||||
realm-detail.hostname=Hostname
|
||||
|
|
|
@ -1087,6 +1087,8 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
|
|||
$scope.realm.accessTokenLifespanForImplicitFlow = TimeUnit2.asUnit(realm.accessTokenLifespanForImplicitFlow);
|
||||
$scope.realm.ssoSessionIdleTimeout = TimeUnit2.asUnit(realm.ssoSessionIdleTimeout);
|
||||
$scope.realm.ssoSessionMaxLifespan = TimeUnit2.asUnit(realm.ssoSessionMaxLifespan);
|
||||
$scope.realm.ssoSessionIdleTimeoutRememberMe = TimeUnit2.asUnit(realm.ssoSessionIdleTimeoutRememberMe);
|
||||
$scope.realm.ssoSessionMaxLifespanRememberMe = TimeUnit2.asUnit(realm.ssoSessionMaxLifespanRememberMe);
|
||||
$scope.realm.offlineSessionIdleTimeout = TimeUnit2.asUnit(realm.offlineSessionIdleTimeout);
|
||||
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||
$scope.realm.offlineSessionMaxLifespan = TimeUnit2.asUnit(realm.offlineSessionMaxLifespan);
|
||||
|
@ -1140,6 +1142,8 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
|
|||
$scope.realm.accessTokenLifespanForImplicitFlow = $scope.realm.accessTokenLifespanForImplicitFlow.toSeconds();
|
||||
$scope.realm.ssoSessionIdleTimeout = $scope.realm.ssoSessionIdleTimeout.toSeconds();
|
||||
$scope.realm.ssoSessionMaxLifespan = $scope.realm.ssoSessionMaxLifespan.toSeconds();
|
||||
$scope.realm.ssoSessionIdleTimeoutRememberMe = $scope.realm.ssoSessionIdleTimeoutRememberMe.toSeconds();
|
||||
$scope.realm.ssoSessionMaxLifespanRememberMe = $scope.realm.ssoSessionMaxLifespanRememberMe.toSeconds();
|
||||
$scope.realm.offlineSessionIdleTimeout = $scope.realm.offlineSessionIdleTimeout.toSeconds();
|
||||
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||
$scope.realm.offlineSessionMaxLifespan = $scope.realm.offlineSessionMaxLifespan.toSeconds();
|
||||
|
|
|
@ -71,6 +71,38 @@
|
|||
<kc-tooltip>{{:: 'sso-session-max.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="ssoSessionIdleTimeoutRememberMe">{{:: 'sso-session-idle-remember-me' | translate}}</label>
|
||||
|
||||
<div class="col-md-6 time-selector">
|
||||
<input class="form-control" type="number" required min="0"
|
||||
max="31536000" data-ng-model="realm.ssoSessionIdleTimeoutRememberMe.time"
|
||||
id="ssoSessionIdleTimeoutRememberMe" name="ssoSessionIdleTimeoutRememberMe"/>
|
||||
<select class="form-control" name="ssoSessionIdleTimeoutRememberMe" data-ng-model="realm.ssoSessionIdleTimeoutRememberMe.unit">
|
||||
<option value="Minutes">{{:: 'minutes' | translate}}</option>
|
||||
<option value="Hours">{{:: 'hours' | translate}}</option>
|
||||
<option value="Days">{{:: 'days' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'sso-session-idle-remember-me.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="ssoSessionMaxLifespanRememberMe">{{:: 'sso-session-max-remember-me' | translate}}</label>
|
||||
|
||||
<div class="col-md-6 time-selector">
|
||||
<input class="form-control" type="number" required min="0"
|
||||
max="31536000" data-ng-model="realm.ssoSessionMaxLifespanRememberMe.time"
|
||||
id="ssoSessionMaxLifespanRememberMe" name="ssoSessionMaxLifespanRememberMe"/>
|
||||
<select class="form-control" name="ssoSessionMaxLifespanRememberMeUnit" data-ng-model="realm.ssoSessionMaxLifespanRememberMe.unit">
|
||||
<option value="Minutes">{{:: 'minutes' | translate}}</option>
|
||||
<option value="Hours">{{:: 'hours' | translate}}</option>
|
||||
<option value="Days">{{:: 'days' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'sso-session-max-remember-me.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="offlineSessionIdleTimeout">{{:: 'offline-session-idle' | translate}}</label>
|
||||
|
||||
|
|
Loading…
Reference in a new issue