From f907a749aad52c6eb722950beae57ab7df07eb36 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Tue, 3 Mar 2015 14:43:16 +0100 Subject: [PATCH] KEYCLOAK-1015 Allow configuring login timeout separate to login actions --- .../META-INF/jpa-changelog-1.2.0.Beta1.xml | 4 ++ .../idm/RealmRepresentation.java | 9 +++ .../base/resources/js/controllers/realm.js | 8 +++ .../base/resources/partials/realm-tokens.html | 21 ++++++- .../java/org/keycloak/models/RealmModel.java | 4 ++ .../keycloak/models/entities/RealmEntity.java | 8 +++ .../models/utils/ModelToRepresentation.java | 1 + .../keycloak/models/utils/RealmInfoUtil.java | 21 +++++++ .../models/utils/RepresentationToModel.java | 8 ++- .../keycloak/models/cache/RealmAdapter.java | 12 ++++ .../models/cache/entities/CachedRealm.java | 5 ++ .../org/keycloak/models/jpa/RealmAdapter.java | 11 ++++ .../models/jpa/entities/RealmEntity.java | 9 +++ .../mongo/keycloak/adapters/RealmAdapter.java | 13 ++++- .../InfinispanUserSessionProvider.java | 4 +- .../sessions/jpa/JpaUserSessionProvider.java | 8 ++- .../sessions/mem/MemUserSessionProvider.java | 4 +- .../mongo/MongoUserSessionProvider.java | 3 +- .../services/managers/ClientSessionCode.java | 14 ++++- .../resources/LoginActionsService.java | 3 + .../testsuite/account/AccountTest.java | 7 +-- .../keycloak/testsuite/admin/RealmTest.java | 2 + .../keycloak/testsuite/forms/LoginTest.java | 42 ++++++++++++-- .../testsuite/forms/LoginTotpTest.java | 29 +++------- .../model/UserSessionProviderTest.java | 55 +++++++++++++++++++ .../oauth/AuthorizationCodeTest.java | 2 +- 26 files changed, 260 insertions(+), 47 deletions(-) create mode 100644 model/api/src/main/java/org/keycloak/models/utils/RealmInfoUtil.java diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml index f5109227e9..609ef45422 100755 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml @@ -90,5 +90,9 @@ + + + + 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 ae57f06565..dce6df6969 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -19,6 +19,7 @@ public class RealmRepresentation { protected Integer ssoSessionMaxLifespan; protected Integer accessCodeLifespan; protected Integer accessCodeLifespanUserAction; + protected Integer accessCodeLifespanLogin; protected Boolean enabled; protected String sslRequired; protected Boolean passwordCredentialGrantAllowed; @@ -199,6 +200,14 @@ public class RealmRepresentation { this.accessCodeLifespanUserAction = accessCodeLifespanUserAction; } + public Integer getAccessCodeLifespanLogin() { + return accessCodeLifespanLogin; + } + + public void setAccessCodeLifespanLogin(Integer accessCodeLifespanLogin) { + this.accessCodeLifespanLogin = accessCodeLifespanLogin; + } + public List getDefaultRoles() { return defaultRoles; } diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/realm.js index 140cbbe9d8..7c5090db2b 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/realm.js +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/realm.js @@ -826,6 +826,12 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http, $scope.realm.accessCodeLifespan = TimeUnit.convert($scope.realm.accessCodeLifespan, from, to); }); + $scope.realm.accessCodeLifespanLoginUnit = TimeUnit.autoUnit(realm.accessCodeLifespanLogin); + $scope.realm.accessCodeLifespanLogin = TimeUnit.toUnit(realm.accessCodeLifespanLogin, $scope.realm.accessCodeLifespanLoginUnit); + $scope.$watch('realm.accessCodeLifespanLoginUnit', function(to, from) { + $scope.realm.accessCodeLifespanLogin = TimeUnit.convert($scope.realm.accessCodeLifespanLogin, from, to); + }); + $scope.realm.accessCodeLifespanUserActionUnit = TimeUnit.autoUnit(realm.accessCodeLifespanUserAction); $scope.realm.accessCodeLifespanUserAction = TimeUnit.toUnit(realm.accessCodeLifespanUserAction, $scope.realm.accessCodeLifespanUserActionUnit); $scope.$watch('realm.accessCodeLifespanUserActionUnit', function(to, from) { @@ -848,12 +854,14 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http, delete realmCopy["accessCodeLifespanUnit"]; delete realmCopy["ssoSessionIdleTimeoutUnit"]; delete realmCopy["accessCodeLifespanUserActionUnit"]; + delete realmCopy["accessCodeLifespanLoginUnit"]; realmCopy.accessTokenLifespan = TimeUnit.toSeconds($scope.realm.accessTokenLifespan, $scope.realm.accessTokenLifespanUnit) realmCopy.ssoSessionIdleTimeout = TimeUnit.toSeconds($scope.realm.ssoSessionIdleTimeout, $scope.realm.ssoSessionIdleTimeoutUnit) realmCopy.ssoSessionMaxLifespan = TimeUnit.toSeconds($scope.realm.ssoSessionMaxLifespan, $scope.realm.ssoSessionMaxLifespanUnit) realmCopy.accessCodeLifespan = TimeUnit.toSeconds($scope.realm.accessCodeLifespan, $scope.realm.accessCodeLifespanUnit) realmCopy.accessCodeLifespanUserAction = TimeUnit.toSeconds($scope.realm.accessCodeLifespanUserAction, $scope.realm.accessCodeLifespanUserActionUnit) + realmCopy.accessCodeLifespanLogin = TimeUnit.toSeconds($scope.realm.accessCodeLifespanLogin, $scope.realm.accessCodeLifespanLoginUnit) Realm.update(realmCopy, function () { $route.reload(); diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-tokens.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-tokens.html index c322bf2c54..2f30127aef 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-tokens.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-tokens.html @@ -92,6 +92,25 @@ +
+ +
+
+
+ +
+
+ +
+
+
+ +
@@ -109,7 +128,7 @@
- +
diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java index d002fd0bed..4212e3babf 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -99,6 +99,10 @@ public interface RealmModel extends RoleContainerModel { void setAccessCodeLifespanUserAction(int seconds); + int getAccessCodeLifespanLogin(); + + void setAccessCodeLifespanLogin(int seconds); + String getPublicKeyPem(); void setPublicKeyPem(String publicKeyPem); diff --git a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java index 47a97a84a5..17792e57ef 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java @@ -34,6 +34,7 @@ public class RealmEntity extends AbstractIdentifiableEntity { private int accessTokenLifespan; private int accessCodeLifespan; private int accessCodeLifespanUserAction; + private int accessCodeLifespanLogin; private int notBefore; private String publicKeyPem; @@ -230,6 +231,13 @@ public class RealmEntity extends AbstractIdentifiableEntity { public void setAccessCodeLifespanUserAction(int accessCodeLifespanUserAction) { this.accessCodeLifespanUserAction = accessCodeLifespanUserAction; } + public int getAccessCodeLifespanLogin() { + return accessCodeLifespanLogin; + } + + public void setAccessCodeLifespanLogin(int accessCodeLifespanLogin) { + this.accessCodeLifespanLogin = accessCodeLifespanLogin; + } public int getNotBefore() { return notBefore; diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index c90afb5836..d0963dfb4f 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -114,6 +114,7 @@ public class ModelToRepresentation { rep.setSsoSessionMaxLifespan(realm.getSsoSessionMaxLifespan()); rep.setAccessCodeLifespan(realm.getAccessCodeLifespan()); rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction()); + rep.setAccessCodeLifespanLogin(realm.getAccessCodeLifespanLogin()); rep.setSmtpServer(realm.getSmtpConfig()); rep.setBrowserSecurityHeaders(realm.getBrowserSecurityHeaders()); rep.setAccountTheme(realm.getAccountTheme()); diff --git a/model/api/src/main/java/org/keycloak/models/utils/RealmInfoUtil.java b/model/api/src/main/java/org/keycloak/models/utils/RealmInfoUtil.java new file mode 100644 index 0000000000..e3e30dd00a --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/utils/RealmInfoUtil.java @@ -0,0 +1,21 @@ +package org.keycloak.models.utils; + +import org.keycloak.models.RealmModel; + +/** + * @author Stian Thorgersen + */ +public class RealmInfoUtil { + + public static int getDettachedClientSessionLifespan(RealmModel realm) { + int lifespan = realm.getAccessCodeLifespanLogin(); + if (realm.getAccessCodeLifespanUserAction() > lifespan) { + lifespan = realm.getAccessCodeLifespanUserAction(); + } + if (realm.getAccessCodeLifespan() > lifespan) { + lifespan = realm.getAccessCodeLifespan(); + } + return lifespan; + } + +} diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index a986de55da..0bfa82ba91 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -78,6 +78,10 @@ public class RepresentationToModel { newRealm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction()); else newRealm.setAccessCodeLifespanUserAction(300); + if (rep.getAccessCodeLifespanLogin() != null) + newRealm.setAccessCodeLifespanLogin(rep.getAccessCodeLifespanLogin()); + else newRealm.setAccessCodeLifespanLogin(1800); + if (rep.getSslRequired() != null) newRealm.setSslRequired(SslRequired.valueOf(rep.getSslRequired().toUpperCase())); if (rep.isPasswordCredentialGrantAllowed() != null) newRealm.setPasswordCredentialGrantAllowed(rep.isPasswordCredentialGrantAllowed()); if (rep.isRegistrationAllowed() != null) newRealm.setRegistrationAllowed(rep.isRegistrationAllowed()); @@ -258,8 +262,8 @@ public class RepresentationToModel { if (rep.isResetPasswordAllowed() != null) realm.setResetPasswordAllowed(rep.isResetPasswordAllowed()); if (rep.getSslRequired() != null) realm.setSslRequired(SslRequired.valueOf(rep.getSslRequired().toUpperCase())); if (rep.getAccessCodeLifespan() != null) realm.setAccessCodeLifespan(rep.getAccessCodeLifespan()); - if (rep.getAccessCodeLifespanUserAction() != null) - realm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction()); + if (rep.getAccessCodeLifespanUserAction() != null) realm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction()); + if (rep.getAccessCodeLifespanLogin() != null) realm.setAccessCodeLifespanLogin(rep.getAccessCodeLifespanLogin()); if (rep.getNotBefore() != null) realm.setNotBefore(rep.getNotBefore()); if (rep.getAccessTokenLifespan() != null) realm.setAccessTokenLifespan(rep.getAccessTokenLifespan()); if (rep.getSsoSessionIdleTimeout() != null) realm.setSsoSessionIdleTimeout(rep.getSsoSessionIdleTimeout()); diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java index 9ade50f3d0..57013cfbd2 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java @@ -300,6 +300,18 @@ public class RealmAdapter implements RealmModel { updated.setAccessCodeLifespanUserAction(seconds); } + @Override + public int getAccessCodeLifespanLogin() { + if (updated != null) return updated.getAccessCodeLifespanLogin(); + return cached.getAccessCodeLifespanLogin(); + } + + @Override + public void setAccessCodeLifespanLogin(int seconds) { + getDelegateForUpdate(); + updated.setAccessCodeLifespanLogin(seconds); + } + @Override public String getPublicKeyPem() { if (updated != null) return updated.getPublicKeyPem(); diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java index 12cb2c5a06..8089e4eb94 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java @@ -53,6 +53,7 @@ public class CachedRealm { private int accessTokenLifespan; private int accessCodeLifespan; private int accessCodeLifespanUserAction; + private int accessCodeLifespanLogin; private int notBefore; private PasswordPolicy passwordPolicy; @@ -111,6 +112,7 @@ public class CachedRealm { accessTokenLifespan = model.getAccessTokenLifespan(); accessCodeLifespan = model.getAccessCodeLifespan(); accessCodeLifespanUserAction = model.getAccessCodeLifespanUserAction(); + accessCodeLifespanLogin = model.getAccessCodeLifespanLogin(); notBefore = model.getNotBefore(); passwordPolicy = model.getPasswordPolicy(); @@ -266,6 +268,9 @@ public class CachedRealm { public int getAccessCodeLifespanUserAction() { return accessCodeLifespanUserAction; } + public int getAccessCodeLifespanLogin() { + return accessCodeLifespanLogin; + } public String getPublicKeyPem() { return publicKeyPem; 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 f0564496b0..9c0e75135f 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 @@ -362,6 +362,17 @@ public class RealmAdapter implements RealmModel { em.flush(); } + @Override + public int getAccessCodeLifespanLogin() { + return realm.getAccessCodeLifespanLogin(); + } + + @Override + public void setAccessCodeLifespanLogin(int accessCodeLifespanLogin) { + realm.setAccessCodeLifespanLogin(accessCodeLifespanLogin); + em.flush(); + } + @Override public String getPublicKeyPem() { return realm.getPublicKeyPem(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java index 258b88f346..9a4358b216 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java @@ -68,6 +68,8 @@ public class RealmEntity { protected int accessCodeLifespan; @Column(name="USER_ACTION_LIFESPAN") protected int accessCodeLifespanUserAction; + @Column(name="LOGIN_LIFESPAN") + protected int accessCodeLifespanLogin; @Column(name="NOT_BEFORE") protected int notBefore; @@ -244,6 +246,13 @@ public class RealmEntity { public void setAccessCodeLifespanUserAction(int accessCodeLifespanUserAction) { this.accessCodeLifespanUserAction = accessCodeLifespanUserAction; } + public int getAccessCodeLifespanLogin() { + return accessCodeLifespanLogin; + } + + public void setAccessCodeLifespanLogin(int accessCodeLifespanLogin) { + this.accessCodeLifespanLogin = accessCodeLifespanLogin; + } public String getPublicKeyPem() { return publicKeyPem; diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index c149e9394e..a51f8013d7 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -295,8 +295,6 @@ public class RealmAdapter extends AbstractMongoAdapter impleme updateRealm(); } - - @Override public int getAccessCodeLifespan() { return realm.getAccessCodeLifespan(); @@ -319,6 +317,17 @@ public class RealmAdapter extends AbstractMongoAdapter impleme updateRealm(); } + @Override + public void setAccessCodeLifespanLogin(int accessCodeLifespanLogin) { + realm.setAccessCodeLifespanLogin(accessCodeLifespanLogin); + updateRealm(); + } + + @Override + public int getAccessCodeLifespanLogin() { + return realm.getAccessCodeLifespanLogin(); + } + @Override public String getPublicKeyPem() { return realm.getPublicKeyPem(); diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java index 96e7f853cc..42dc3aa356 100644 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java @@ -22,6 +22,7 @@ import org.keycloak.models.sessions.infinispan.mapreduce.LargestResultReducer; import org.keycloak.models.sessions.infinispan.mapreduce.SessionMapper; import org.keycloak.models.sessions.infinispan.mapreduce.UserSessionMapper; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.RealmInfoUtil; import org.keycloak.util.Time; import java.util.Collection; @@ -201,6 +202,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { public void removeExpiredUserSessions(RealmModel realm) { int expired = Time.currentTime() - realm.getSsoSessionMaxLifespan(); int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout(); + int expiredDettachedClientSession = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm); Map map = new MapReduceTask(sessionCache) .mappedWith(UserSessionMapper.create(realm.getId()).expired(expired, expiredRefresh).emitKey()) @@ -212,7 +214,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } map = new MapReduceTask(sessionCache) - .mappedWith(ClientSessionMapper.create(realm.getId()).expiredRefresh(expiredRefresh).requireNullUserSession(true).emitKey()) + .mappedWith(ClientSessionMapper.create(realm.getId()).expiredRefresh(expiredDettachedClientSession).requireNullUserSession(true).emitKey()) .reducedWith(new FirstResultReducer()) .execute(); diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java index 169a92d567..dcae8e2579 100755 --- a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java +++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java @@ -12,6 +12,7 @@ import org.keycloak.models.sessions.jpa.entities.ClientSessionEntity; import org.keycloak.models.sessions.jpa.entities.UserSessionEntity; import org.keycloak.models.sessions.jpa.entities.UsernameLoginFailureEntity; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.RealmInfoUtil; import org.keycloak.util.Time; import javax.persistence.EntityManager; @@ -190,18 +191,19 @@ public class JpaUserSessionProvider implements UserSessionProvider { public void removeExpiredUserSessions(RealmModel realm) { int maxTime = Time.currentTime() - realm.getSsoSessionMaxLifespan(); int idleTime = Time.currentTime() - realm.getSsoSessionIdleTimeout(); + int dettachedClientSessionExpired = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm); em.createNamedQuery("removeDetachedClientSessionRoleByExpired") .setParameter("realmId", realm.getId()) - .setParameter("maxTime", idleTime) + .setParameter("maxTime", dettachedClientSessionExpired) .executeUpdate(); em.createNamedQuery("removeDetachedClientSessionNoteByExpired") .setParameter("realmId", realm.getId()) - .setParameter("maxTime", idleTime) + .setParameter("maxTime", dettachedClientSessionExpired) .executeUpdate(); em.createNamedQuery("removeDetachedClientSessionByExpired") .setParameter("realmId", realm.getId()) - .setParameter("maxTime", idleTime) + .setParameter("maxTime", dettachedClientSessionExpired) .executeUpdate(); em.createNamedQuery("removeClientSessionRoleByExpired") .setParameter("realmId", realm.getId()) diff --git a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java index cd5eef1534..41081c0c6d 100755 --- a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java +++ b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java @@ -14,6 +14,7 @@ import org.keycloak.models.sessions.mem.entities.UserSessionEntity; import org.keycloak.models.sessions.mem.entities.UsernameLoginFailureEntity; import org.keycloak.models.sessions.mem.entities.UsernameLoginFailureKey; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.RealmInfoUtil; import org.keycloak.util.Time; import java.util.Collections; @@ -191,10 +192,11 @@ public class MemUserSessionProvider implements UserSessionProvider { } } } + int expired = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm); Iterator citr = clientSessions.values().iterator(); while (citr.hasNext()) { ClientSessionEntity c = citr.next(); - if (c.getSession() == null && c.getRealmId().equals(realm.getId()) && c.getTimestamp() < Time.currentTime() - realm.getSsoSessionIdleTimeout()) { + if (c.getSession() == null && c.getRealmId().equals(realm.getId()) && c.getTimestamp() < expired) { citr.remove(); } } diff --git a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java index 6ddebb9811..6630855871 100755 --- a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java +++ b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java @@ -17,6 +17,7 @@ import org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity; import org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity; import org.keycloak.models.sessions.mongo.entities.MongoUsernameLoginFailureEntity; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.RealmInfoUtil; import org.keycloak.util.Time; import java.util.LinkedList; @@ -190,7 +191,7 @@ public class MongoUserSessionProvider implements UserSessionProvider { query = new QueryBuilder() .and("sessionId").is(null) .and("realmId").is(realm.getId()) - .and("timestamp").lessThan(currentTime - realm.getSsoSessionIdleTimeout()) + .and("timestamp").lessThan(currentTime - RealmInfoUtil.getDettachedClientSessionLifespan(realm)) .get(); mongoStore.removeEntities(MongoClientSessionEntity.class, query, invocationContext); diff --git a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java index 24861f3157..9e52c9749b 100755 --- a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java +++ b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java @@ -91,7 +91,19 @@ public class ClientSessionCode { return false; } - int lifespan = action.equals(ClientSessionModel.Action.CODE_TO_TOKEN) ? realm.getAccessCodeLifespan() : realm.getAccessCodeLifespanUserAction(); + int lifespan; + switch (action) { + case CODE_TO_TOKEN: + lifespan = realm.getAccessCodeLifespan(); + break; + case AUTHENTICATE: + lifespan = realm.getAccessCodeLifespanLogin() > 0 ? realm.getAccessCodeLifespanLogin() : realm.getAccessCodeLifespanUserAction(); + break; + default: + lifespan = realm.getAccessCodeLifespanUserAction(); + break; + } + return timestamp + lifespan > Time.currentTime(); } diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index d5d4867ae2..3590f71bf0 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -272,7 +272,10 @@ public class LoginActionsService { event.error(Errors.INVALID_CODE); return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown code, please login again through your application."); } + ClientSessionModel clientSession = clientCode.getClientSession(); + event.detail(Details.CODE_ID, clientSession.getId()); + if (!clientCode.isValid(ClientSessionModel.Action.AUTHENTICATE) || clientSession.getUserSession() != null) { clientCode.setAction(ClientSessionModel.Action.AUTHENTICATE); event.client(clientSession.getClient()).error(Errors.EXPIRED_CODE); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java index 722717f0eb..504b536430 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java @@ -157,11 +157,6 @@ public class AccountTest { }); } - @Test @Ignore - public void runit() throws Exception { - Thread.sleep(10000000); - } - @Test public void returnToAppFromQueryParam() { driver.navigate().to(AccountUpdateProfilePage.PATH + "?referrer=test-app"); @@ -224,7 +219,7 @@ public class AccountTest { Assert.assertEquals("Invalid username or password.", loginPage.getError()); - events.expectLogin().session((String) null).error("invalid_user_credentials").removeDetail(Details.CODE_ID).assertEvent(); + events.expectLogin().session((String) null).error("invalid_user_credentials").assertEvent(); loginPage.open(); loginPage.login("test-user@localhost", "new-password"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java index 6626b60ab6..5dd37bc03d 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java @@ -63,6 +63,7 @@ public class RealmTest extends AbstractClientTest { RealmRepresentation rep = realm.toRepresentation(); rep.setSsoSessionIdleTimeout(123); rep.setSsoSessionMaxLifespan(12); + rep.setAccessCodeLifespanLogin(1234); realm.update(rep); @@ -70,6 +71,7 @@ public class RealmTest extends AbstractClientTest { assertEquals(123, rep.getSsoSessionIdleTimeout().intValue()); assertEquals(12, rep.getSsoSessionMaxLifespan().intValue()); + assertEquals(1234, rep.getAccessCodeLifespanLogin().intValue()); } @Test diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java index a1ef54c82d..75a83f4f8a 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java @@ -116,7 +116,7 @@ public class LoginTest { Assert.assertEquals("Invalid username or password.", loginPage.getError()); - events.expectLogin().user(userId).session((String) null).error("invalid_user_credentials").detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).assertEvent(); + events.expectLogin().user(userId).session((String) null).error("invalid_user_credentials").detail(Details.USERNAME, "login-test").assertEvent(); } @Test @@ -136,7 +136,7 @@ public class LoginTest { Assert.assertEquals("Invalid username or password.", loginPage.getError()); - events.expectLogin().user(userId).session((String) null).error("invalid_user_credentials").detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).assertEvent(); + events.expectLogin().user(userId).session((String) null).error("invalid_user_credentials").detail(Details.USERNAME, "login-test").assertEvent(); } finally { keycloakRule.configure(new KeycloakRule.KeycloakSetup() { @Override @@ -164,7 +164,7 @@ public class LoginTest { Assert.assertEquals("Account is disabled, contact admin", loginPage.getError()); - events.expectLogin().user(userId).session((String) null).error("user_disabled").detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).assertEvent(); + events.expectLogin().user(userId).session((String) null).error("user_disabled").detail(Details.USERNAME, "login-test").assertEvent(); } finally { keycloakRule.configure(new KeycloakRule.KeycloakSetup() { @Override @@ -184,7 +184,7 @@ public class LoginTest { Assert.assertEquals("Invalid username or password.", loginPage.getError()); - events.expectLogin().user((String) null).session((String) null).error("user_not_found").detail(Details.USERNAME, "invalid").removeDetail(Details.CODE_ID).assertEvent(); + events.expectLogin().user((String) null).session((String) null).error("user_not_found").detail(Details.USERNAME, "invalid").assertEvent(); } @Test @@ -198,6 +198,36 @@ public class LoginTest { events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent(); } + @Test + public void loginNoTimeoutWithLongWait() { + try { + loginPage.open(); + + Time.setOffset(1700); + + loginPage.login("login-test", "password"); + + events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId(); + } finally { + Time.setOffset(0); + } + } + + @Test + public void loginTimeout() { + try { + loginPage.open(); + + Time.setOffset(1850); + + loginPage.login("login-test", "password"); + + events.expectLogin().clearDetails().detail(Details.CODE_ID, AssertEvents.isCodeId()).user((String) null).session((String) null).error("expired_code").assertEvent().getSessionId(); + } finally { + Time.setOffset(0); + } + } + @Test public void loginLoginHint() { String loginFormUrl = oauth.getLoginFormUrl() + "&login_hint=login-test"; @@ -274,7 +304,7 @@ public class LoginTest { Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertEquals("access_denied", oauth.getCurrentQuery().get(OAuth2Constants.ERROR)); - events.expectLogin().error("rejected_by_user").user((String) null).session((String) null).removeDetail(Details.USERNAME).removeDetail(Details.CODE_ID).assertEvent(); + events.expectLogin().error("rejected_by_user").user((String) null).session((String) null).removeDetail(Details.USERNAME).assertEvent(); } // KEYCLOAK-1037 @@ -288,7 +318,7 @@ public class LoginTest { loginPage.assertCurrent(); Assert.assertEquals("Login timeout. Please login again", loginPage.getError()); - events.expectLogin().user((String) null).session((String) null).error("expired_code").clearDetails().assertEvent(); + events.expectLogin().user((String) null).session((String) null).error("expired_code").clearDetails().detail(Details.CODE_ID, AssertEvents.isCodeId()).assertEvent(); } finally { Time.setOffset(0); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java index 580ba2abdc..45795dac81 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java @@ -43,6 +43,7 @@ import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup; import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebRule; +import org.keycloak.util.Time; import org.openqa.selenium.WebDriver; import java.net.MalformedURLException; @@ -113,7 +114,7 @@ public class LoginTotpTest { loginPage.assertCurrent(); Assert.assertEquals("Invalid username or password.", loginPage.getError()); - events.expectLogin().error("invalid_user_credentials").removeDetail(Details.CODE_ID).session((String) null).assertEvent(); + events.expectLogin().error("invalid_user_credentials").session((String) null).assertEvent(); } @Test @@ -139,45 +140,29 @@ public class LoginTotpTest { Assert.assertEquals("Invalid username or password.", loginPage.getError()); - events.expectLogin().error("invalid_user_credentials").removeDetail(Details.CODE_ID).session((String) null).assertEvent(); + events.expectLogin().error("invalid_user_credentials").session((String) null).assertEvent(); } @Test public void loginWithTotpExpiredPasswordToken() throws Exception { try { - loginPage.open(); loginPage.login("test-user@localhost", "password"); - keycloakRule.configure(new KeycloakSetup() { - @Override - public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - lifespan = appRealm.getAccessCodeLifespanUserAction(); - appRealm.setAccessCodeLifespanUserAction(1); - } - }); - loginTotpPage.assertCurrent(); - Thread.sleep(2000); + Time.setOffset(350); loginTotpPage.login(totp.generate("totpSecret")); loginPage.assertCurrent(); - Assert.assertEquals("Login timeout. Please login again", loginPage.getError()); + Assert.assertEquals("Invalid username or password.", loginPage.getError()); - AssertEvents.ExpectedEvent expectedEvent = events.expectLogin().error("expired_code") - .user((String)null) - .clearDetails() + AssertEvents.ExpectedEvent expectedEvent = events.expectLogin().error("invalid_user_credentials") .session((String) null); expectedEvent.assertEvent(); } finally { - keycloakRule.configure(new KeycloakSetup() { - @Override - public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - appRealm.setAccessCodeLifespanUserAction(lifespan); - } - }); + Time.setOffset(0); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java index db5d370a70..228416d829 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java @@ -293,6 +293,61 @@ public class UserSessionProviderTest { } } + @Test + public void testExpireDetachedClientSessions() { + try { + realm.setAccessCodeLifespan(10); + realm.setAccessCodeLifespanUserAction(10); + realm.setAccessCodeLifespanLogin(30); + + // Login lifespan is largest + String clientSessionId = session.sessions().createClientSession(realm, realm.findClient("test-app")).getId(); + + Time.setOffset(25); + session.sessions().removeExpiredUserSessions(realm); + assertNotNull(session.sessions().getClientSession(clientSessionId)); + + Time.setOffset(35); + session.sessions().removeExpiredUserSessions(realm); + assertNull(session.sessions().getClientSession(clientSessionId)); + + // User action is largest + realm.setAccessCodeLifespanUserAction(40); + + Time.setOffset(0); + clientSessionId = session.sessions().createClientSession(realm, realm.findClient("test-app")).getId(); + + Time.setOffset(35); + session.sessions().removeExpiredUserSessions(realm); + assertNotNull(session.sessions().getClientSession(clientSessionId)); + + Time.setOffset(45); + session.sessions().removeExpiredUserSessions(realm); + assertNull(session.sessions().getClientSession(clientSessionId)); + + // Access code is largest + realm.setAccessCodeLifespan(50); + + Time.setOffset(0); + clientSessionId = session.sessions().createClientSession(realm, realm.findClient("test-app")).getId(); + + Time.setOffset(45); + session.sessions().removeExpiredUserSessions(realm); + assertNotNull(session.sessions().getClientSession(clientSessionId)); + + Time.setOffset(55); + session.sessions().removeExpiredUserSessions(realm); + assertNull(session.sessions().getClientSession(clientSessionId)); + } finally { + Time.setOffset(0); + + realm.setAccessCodeLifespan(60); + realm.setAccessCodeLifespanUserAction(300); + realm.setAccessCodeLifespanLogin(1800); + + } + } + @Test public void testGetByClient() { UserSessionModel[] sessions = createSessions(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java index 7ecd29268e..8048ed0741 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java @@ -140,7 +140,7 @@ public class AuthorizationCodeTest { assertEquals("access_denied", error); events.expectLogin().error("rejected_by_user").user((String) null).session((String) null) - .removeDetail(Details.USERNAME).removeDetail(Details.CODE_ID) + .removeDetail(Details.USERNAME) .detail(Details.REDIRECT_URI, "http://localhost:8081/auth/realms/test/protocol/openid-connect/oauth/oob") .assertEvent().getDetails().get(Details.CODE_ID);