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:
Thomas Darimont 2016-08-23 00:40:23 +02:00 committed by Marek Posolda
parent 8c650f9f6a
commit cf57a1bc4b
25 changed files with 435 additions and 15 deletions

View file

@ -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;
}

View file

@ -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();

View file

@ -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;
}

View file

@ -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>() {

View file

@ -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;
}

View file

@ -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();

View file

@ -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;
}

View file

@ -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>

View file

@ -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>

View file

@ -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());

View file

@ -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

View file

@ -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);

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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);

View file

@ -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()) {

View file

@ -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());

View file

@ -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);
}
}
}

View file

@ -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();

View file

@ -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;

View file

@ -3,6 +3,10 @@
"enabled": true,
"accessTokenLifespan": 6000,
"accessTokenLifespanForImplicitFlow": 1500,
"ssoSessionIdleTimeout": 1800,
"ssoSessionMaxLifespan": 36000,
"ssoSessionIdleTimeoutRememberMe": 3600,
"ssoSessionMaxLifespanRememberMe": 172800,
"accessCodeLifespan": 30,
"accessCodeLifespanUserAction": 600,
"offlineSessionIdleTimeout": 3600000,

View file

@ -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

View file

@ -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();

View file

@ -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>