From f907a749aad52c6eb722950beae57ab7df07eb36 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Tue, 3 Mar 2015 14:43:16 +0100 Subject: [PATCH 1/4] 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); From 3d1db7dd9142d634a09b30b82bc012564f83fe14 Mon Sep 17 00:00:00 2001 From: Marko Strukelj Date: Wed, 4 Mar 2015 12:40:06 +0100 Subject: [PATCH 2/4] Fix boolean type - was causing issues on some dbs i.e. PostgreSQL --- .../META-INF/jpa-changelog-1.2.0.Beta1.xml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 609ef45422..f1a35c9ba1 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 @@ -14,8 +14,8 @@ - - + + @@ -46,13 +46,13 @@ - + - - - + + + @@ -71,7 +71,7 @@ - + From 6c7f35c5091b7e363f3ef277bb00e627f57b5b3f Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 4 Mar 2015 13:39:00 +0100 Subject: [PATCH 3/4] KEYCLOAK-1014 Don't redirect to app after reset password or verify email if new browser session --- .../main/resources/theme/login/base/info.ftl | 15 +++ .../login/base/messages/messages.properties | 3 + .../org/keycloak/login/LoginFormsPages.java | 2 +- .../keycloak/login/LoginFormsProvider.java | 2 + .../FreeMarkerLoginFormsProvider.java | 11 +- .../keycloak/login/freemarker/Templates.java | 2 + .../login/freemarker/model/ClientBean.java | 8 ++ .../managers/AuthenticationManager.java | 2 + .../resources/LoginActionsService.java | 54 ++++++++- .../RequiredActionEmailVerificationTest.java | 44 ++++++++ .../testsuite/forms/ResetPasswordTest.java | 104 +++++++++++++----- .../keycloak/testsuite/pages/InfoPage.java | 53 +++++++++ 12 files changed, 264 insertions(+), 36 deletions(-) create mode 100755 forms/common-themes/src/main/resources/theme/login/base/info.ftl create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/pages/InfoPage.java diff --git a/forms/common-themes/src/main/resources/theme/login/base/info.ftl b/forms/common-themes/src/main/resources/theme/login/base/info.ftl new file mode 100755 index 0000000000..d303e5b955 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/login/base/info.ftl @@ -0,0 +1,15 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=false; section> + <#if section = "title"> + ${message.summary} + <#elseif section = "header"> + ${message.summary} + <#elseif section = "form"> +
+

${message.summary}

+ <#if client.baseUrl??> +

${rb.backToApplication}

+ +
+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties b/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties index 3a1c04113f..689973145a 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties +++ b/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties @@ -20,6 +20,7 @@ rememberMe=Remember me passwordConfirm=Confirm password passwordNew=New Password passwordNewConfirm=New Password confirmation +passwordUpdated=Password updated cancel=Cancel accept=Accept submit=Submit @@ -85,6 +86,7 @@ emailVerifyInstr=An email with instructions to verify your email address has bee emailVerifyInstrQ=Haven't received a verification code in your email? emailVerifyClick=Click here emailVerifyResend=to re-send the email. +emailVerified=Email verified error=A system error has occured, contact admin errorTitle=We're sorry... @@ -106,6 +108,7 @@ errorHeader=Error! emailForgotHeader=Forgot Your Password? backToLogin=« Back to Login +backToApplication=« Back to Application emailUpdateHeader=Update password emailSent=You should receive an email shortly with further instructions. emailSendError=Failed to send email, please try again later diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsPages.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsPages.java index 2b0cd23597..e34b0ef755 100644 --- a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsPages.java +++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsPages.java @@ -5,6 +5,6 @@ package org.keycloak.login; */ public enum LoginFormsPages { - LOGIN, LOGIN_TOTP, LOGIN_CONFIG_TOTP, LOGIN_VERIFY_EMAIL, OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, REGISTER, ERROR, LOGIN_UPDATE_PROFILE, CODE; + LOGIN, LOGIN_TOTP, LOGIN_CONFIG_TOTP, LOGIN_VERIFY_EMAIL, OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, REGISTER, INFO, ERROR, LOGIN_UPDATE_PROFILE, CODE; } diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java index 45f3adb1b8..f9a9c481e5 100755 --- a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java +++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java @@ -31,6 +31,8 @@ public interface LoginFormsProvider extends Provider { public Response createRegistration(); + public Response createInfoPage(); + public Response createErrorPage(); public Response createOAuthGrant(); diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java index 44a7585004..f8b85c689f 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -29,6 +29,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.services.messages.Messages; +import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.resources.flows.Urls; import javax.ws.rs.core.MediaType; @@ -51,7 +52,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { private static final Logger logger = Logger.getLogger(FreeMarkerLoginFormsProvider.class); - private String message; + public static enum MessageType {SUCCESS, WARNING, ERROR} + private String accessCode; private Response.Status status; private List realmRolesRequested; @@ -61,8 +63,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { private String accessRequestMessage; private URI actionUri; - public static enum MessageType {SUCCESS, WARNING, ERROR} - + private String message; private MessageType messageType = MessageType.ERROR; private MultivaluedMap formData; @@ -253,6 +254,10 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { return createResponse(LoginFormsPages.REGISTER); } + public Response createInfoPage() { + return createResponse(LoginFormsPages.INFO); + } + public Response createErrorPage() { if (status == null) { status = Response.Status.INTERNAL_SERVER_ERROR; diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/Templates.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/Templates.java index 02e20ec3b7..785b1860f7 100644 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/Templates.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/Templates.java @@ -25,6 +25,8 @@ public class Templates { return "login-update-password.ftl"; case REGISTER: return "register.ftl"; + case INFO: + return "info.ftl"; case ERROR: return "error.ftl"; case LOGIN_UPDATE_PROFILE: diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ClientBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ClientBean.java index 450776288c..fcd9e1136c 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ClientBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ClientBean.java @@ -26,4 +26,12 @@ public class ClientBean { public String getClientId() { return client.getClientId(); } + + public String getBaseUrl() { + if (client instanceof ApplicationModel) { + return ((ApplicationModel) client).getBaseUrl(); + } + return null; + } + } diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index d02ad2b9eb..25fce1fa35 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -26,6 +26,7 @@ import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.util.CookieHelper; @@ -366,6 +367,7 @@ public class AuthenticationManager { LoginFormsProvider loginFormsProvider = Flows.forms(session, realm, client, uriInfo).setClientSessionCode(accessCode.getCode()).setUser(user); if (action.equals(UserModel.RequiredAction.VERIFY_EMAIL)) { event.clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail()).success(); + LoginActionsService.createActionCookie(realm, uriInfo, clientConnection, userSession.getId()); } return loginFormsProvider 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 3590f71bf0..8a3abec93f 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -53,6 +53,7 @@ import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.Urls; +import org.keycloak.services.util.CookieHelper; import org.keycloak.services.validation.Validation; import javax.ws.rs.Consumes; @@ -61,6 +62,7 @@ import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; +import javax.ws.rs.core.Cookie; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; @@ -79,6 +81,8 @@ public class LoginActionsService { protected static final Logger logger = Logger.getLogger(LoginActionsService.class); + public static final String ACTION_COOKIE = "KEYCLOAK_ACTION"; + private RealmModel realm; @Context @@ -163,6 +167,18 @@ public class LoginActionsService { } } + boolean check(String code, ClientSessionModel.Action requiredAction, ClientSessionModel.Action alternativeRequiredAction) { + if (!check(code)) { + return false; + } else if (!(clientCode.isValid(requiredAction) || clientCode.isValid(alternativeRequiredAction))) { + event.error(Errors.INVALID_CODE); + response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid code, please login again through your application."); + return false; + } else { + return true; + } + } + public boolean check(String code) { if (!checkSsl()) { event.error(Errors.SSL_REQUIRED); @@ -690,7 +706,7 @@ public class LoginActionsService { final MultivaluedMap formData) { event.event(EventType.UPDATE_PASSWORD); Checks checks = new Checks(); - if (!checks.check(code, ClientSessionModel.Action.UPDATE_PASSWORD)) { + if (!checks.check(code, ClientSessionModel.Action.UPDATE_PASSWORD, ClientSessionModel.Action.RECOVER_PASSWORD)) { return checks.response; } ClientSessionCode accessCode = checks.clientCode; @@ -724,7 +740,17 @@ public class LoginActionsService { user.removeRequiredAction(RequiredAction.UPDATE_PASSWORD); - event.clone().event(EventType.UPDATE_PASSWORD).success(); + event.event(EventType.UPDATE_PASSWORD).success(); + + if (clientSession.getAction().equals(ClientSessionModel.Action.RECOVER_PASSWORD)) { + String actionCookieValue = getActionCookie(); + if (actionCookieValue == null || !actionCookieValue.equals(userSession.getId())) { + return Flows.forms(session, realm, clientSession.getClient(), uriInfo).setSuccess("passwordUpdated").createInfoPage(); + } + } + + event = event.clone().event(EventType.LOGIN); + return redirectOauth(user, accessCode, clientSession, userSession); } @@ -747,7 +773,14 @@ public class LoginActionsService { user.removeRequiredAction(RequiredAction.VERIFY_EMAIL); - event.clone().event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail()).success(); + event.event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail()).success(); + + String actionCookieValue = getActionCookie(); + if (actionCookieValue == null || !actionCookieValue.equals(userSession.getId())) { + return Flows.forms(session, realm, clientSession.getClient(), uriInfo).setSuccess("emailVerified").createInfoPage(); + } + + event = event.clone().removeDetail(Details.EMAIL).event(EventType.LOGIN); return redirectOauth(user, accessCode, clientSession, userSession); } else { @@ -760,6 +793,8 @@ public class LoginActionsService { UserSessionModel userSession = clientSession.getUserSession(); initEvent(clientSession); + createActionCookie(realm, uriInfo, clientConnection, userSession.getId()); + return Flows.forms(session, realm, null, uriInfo) .setClientSessionCode(accessCode.getCode()) .setUser(userSession.getUser()) @@ -777,7 +812,6 @@ public class LoginActionsService { return checks.response; } ClientSessionCode accessCode = checks.clientCode; - accessCode.setRequiredAction(RequiredAction.UPDATE_PASSWORD); return Flows.forms(session, realm, null, uriInfo) .setClientSessionCode(accessCode.getCode()) .createResponse(RequiredAction.UPDATE_PASSWORD); @@ -864,11 +898,23 @@ public class LoginActionsService { .setClientSessionCode(accessCode.getCode()) .createErrorPage(); } + + createActionCookie(realm, uriInfo, clientConnection, userSession.getId()); } return Flows.forms(session, realm, client, uriInfo).setSuccess("emailSent").setClientSessionCode(accessCode.getCode()).createPasswordReset(); } + private String getActionCookie() { + Cookie cookie = headers.getCookies().get(ACTION_COOKIE); + AuthenticationManager.expireCookie(realm, ACTION_COOKIE, AuthenticationManager.getRealmCookiePath(realm, uriInfo), realm.getSslRequired().isRequired(clientConnection), clientConnection); + return cookie != null ? cookie.getValue() : null; + } + + public static void createActionCookie(RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection, String sessionId) { + CookieHelper.addCookie(ACTION_COOKIE, sessionId, AuthenticationManager.getRealmCookiePath(realm, uriInfo), null, null, -1, realm.getSslRequired().isRequired(clientConnection), true); + } + private Response redirectOauth(UserModel user, ClientSessionCode accessCode, ClientSessionModel clientSession, UserSessionModel userSession) { return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java index 0f2b13f599..1d6665171b 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java @@ -37,6 +37,7 @@ import org.keycloak.testsuite.MailUtil; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; +import org.keycloak.testsuite.pages.InfoPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.RegisterPage; import org.keycloak.testsuite.pages.VerifyEmailPage; @@ -51,6 +52,9 @@ import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.io.IOException; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + /** * @author Stian Thorgersen */ @@ -86,6 +90,9 @@ public class RequiredActionEmailVerificationTest { @WebResource protected RegisterPage registerPage; + @WebResource + protected InfoPage infoPage; + @Before public void before() { oauth.state("mystate"); // have to set this as keycloak validates that state is sent @@ -200,4 +207,41 @@ public class RequiredActionEmailVerificationTest { events.expectLogin().session(sessionId).assertEvent(); } + @Test + public void verifyEmailNewBrowserSession() throws IOException, MessagingException { + loginPage.open(); + loginPage.login("test-user@localhost", "password"); + + Assert.assertTrue(verifyEmailPage.isCurrent()); + + Assert.assertEquals(1, greenMail.getReceivedMessages().length); + + MimeMessage message = greenMail.getReceivedMessages()[0]; + + String body = (String) message.getContent(); + String verificationUrl = MailUtil.getLink(body); + + AssertEvents.ExpectedEvent emailEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost"); + Event sendEvent = emailEvent.assertEvent(); + String sessionId = sendEvent.getSessionId(); + + String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID); + + Assert.assertEquals(mailCodeId, verificationUrl.split("key=")[1].split("\\.")[1]); + + driver.manage().deleteAllCookies(); + + driver.navigate().to(verificationUrl.trim()); + + events.expectRequiredAction(EventType.VERIFY_EMAIL).session(sessionId).detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent(); + + assertTrue(infoPage.isCurrent()); + assertEquals("Email verified", infoPage.getInfo()); + + loginPage.open(); + + assertTrue(loginPage.isCurrent()); + } + + } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java index 20926f40c6..7582e2094f 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java @@ -21,7 +21,6 @@ */ package org.keycloak.testsuite.forms; -import org.junit.Assert; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; @@ -41,6 +40,7 @@ import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.ErrorPage; +import org.keycloak.testsuite.pages.InfoPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPasswordResetPage; import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; @@ -56,6 +56,9 @@ import javax.mail.internet.MimeMessage; import java.io.IOException; import java.util.Collections; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + /** * @author Stian Thorgersen */ @@ -103,6 +106,9 @@ public class ResetPasswordTest { @WebResource protected ErrorPage errorPage; + @WebResource + protected InfoPage infoPage; + @WebResource protected LoginPasswordResetPage resetPasswordPage; @@ -132,13 +138,13 @@ public class ResetPasswordTest { resetPasswordPage.backToLogin(); - Assert.assertTrue(loginPage.isCurrent()); + assertTrue(loginPage.isCurrent()); loginPage.login("login-test", "password"); events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent(); - Assert.assertEquals(1, greenMail.getReceivedMessages().length); + assertEquals(1, greenMail.getReceivedMessages().length); MimeMessage message = greenMail.getReceivedMessages()[0]; @@ -149,8 +155,8 @@ public class ResetPasswordTest { events.expect(EventType.RESET_PASSWORD_ERROR).client((String) null).user((String) null).error("invalid_code").clearDetails().assertEvent(); - Assert.assertTrue(errorPage.isCurrent()); - Assert.assertEquals("Unknown code, please login again through your application.", errorPage.getError()); + assertTrue(errorPage.isCurrent()); + assertEquals("Unknown code, please login again through your application.", errorPage.getError()); } @Test @@ -168,7 +174,7 @@ public class ResetPasswordTest { resetPasswordPage.backToLogin(); - Assert.assertTrue(loginPage.isCurrent()); + assertTrue(loginPage.isCurrent()); loginPage.login("login@test.com", "password"); @@ -177,8 +183,8 @@ public class ResetPasswordTest { String code = oauth.getCurrentQuery().get("code"); OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); - Assert.assertEquals(200, tokenResponse.getStatusCode()); - Assert.assertEquals(userId, oauth.verifyToken(tokenResponse.getAccessToken()).getSubject()); + assertEquals(200, tokenResponse.getStatusCode()); + assertEquals(userId, oauth.verifyToken(tokenResponse.getAccessToken()).getSubject()); events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), loginEvent.getSessionId()).user(userId).assertEvent(); } @@ -200,9 +206,9 @@ public class ResetPasswordTest { String sessionId = events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId(); - Assert.assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); + assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); - Assert.assertEquals(1, greenMail.getReceivedMessages().length); + assertEquals(1, greenMail.getReceivedMessages().length); MimeMessage message = greenMail.getReceivedMessages()[0]; @@ -217,7 +223,7 @@ public class ResetPasswordTest { events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).session(sessionId).detail(Details.USERNAME, username).assertEvent(); - Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); events.expectLogin().user(userId).detail(Details.USERNAME, username).session(sessionId).assertEvent(); @@ -231,7 +237,7 @@ public class ResetPasswordTest { events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent(); - Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); } @Test @@ -245,11 +251,11 @@ public class ResetPasswordTest { resetPasswordPage.assertCurrent(); - Assert.assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); + assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); Thread.sleep(1000); - Assert.assertEquals(0, greenMail.getReceivedMessages().length); + assertEquals(0, greenMail.getReceivedMessages().length); events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user((String) null).session((String) null).detail(Details.USERNAME, "invalid").removeDetail(Details.EMAIL).removeDetail(Details.CODE_ID).error("user_not_found").assertEvent(); } @@ -268,9 +274,9 @@ public class ResetPasswordTest { String sessionId = events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId(); - Assert.assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); + assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); - Assert.assertEquals(1, greenMail.getReceivedMessages().length); + assertEquals(1, greenMail.getReceivedMessages().length); MimeMessage message = greenMail.getReceivedMessages()[0]; @@ -283,7 +289,7 @@ public class ResetPasswordTest { errorPage.assertCurrent(); - Assert.assertEquals("Invalid code, please login again through your application.", errorPage.getError()); + assertEquals("Invalid code, please login again through your application.", errorPage.getError()); events.expectRequiredAction(EventType.RESET_PASSWORD).error("invalid_code").client((String) null).user((String) null).session((String) null).clearDetails().assertEvent(); } finally { @@ -310,11 +316,11 @@ public class ResetPasswordTest { resetPasswordPage.assertCurrent(); - Assert.assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); + assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); Thread.sleep(1000); - Assert.assertEquals(0, greenMail.getReceivedMessages().length); + assertEquals(0, greenMail.getReceivedMessages().length); events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("user_disabled").assertEvent(); } finally { @@ -350,11 +356,11 @@ public class ResetPasswordTest { resetPasswordPage.assertCurrent(); - Assert.assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); + assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); Thread.sleep(1000); - Assert.assertEquals(0, greenMail.getReceivedMessages().length); + assertEquals(0, greenMail.getReceivedMessages().length); events.expectRequiredAction(EventType.SEND_RESET_PASSWORD_ERROR).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("invalid_email").assertEvent(); } finally { @@ -388,11 +394,11 @@ public class ResetPasswordTest { errorPage.assertCurrent(); - Assert.assertEquals("Failed to send email, please try again later", errorPage.getError()); + assertEquals("Failed to send email, please try again later", errorPage.getError()); Thread.sleep(1000); - Assert.assertEquals(0, greenMail.getReceivedMessages().length); + assertEquals(0, greenMail.getReceivedMessages().length); events.expectRequiredAction(EventType.SEND_RESET_PASSWORD_ERROR).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error(Errors.EMAIL_SEND_FAILED).assertEvent(); } finally { @@ -423,9 +429,9 @@ public class ResetPasswordTest { resetPasswordPage.assertCurrent(); - Assert.assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); + assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); - Assert.assertEquals(1, greenMail.getReceivedMessages().length); + assertEquals(1, greenMail.getReceivedMessages().length); MimeMessage message = greenMail.getReceivedMessages()[0]; @@ -440,13 +446,13 @@ public class ResetPasswordTest { updatePasswordPage.changePassword("invalid", "invalid"); - Assert.assertEquals("Invalid password: minimum length 8", resetPasswordPage.getErrorMessage()); + assertEquals("Invalid password: minimum length 8", resetPasswordPage.getErrorMessage()); updatePasswordPage.changePassword("resetPasswordWithPasswordPolicy", "resetPasswordWithPasswordPolicy"); events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).session(sessionId).detail(Details.USERNAME, "login-test").assertEvent(); - Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").session(sessionId).assertEvent(); @@ -458,9 +464,51 @@ public class ResetPasswordTest { loginPage.login("login-test", "resetPasswordWithPasswordPolicy"); - Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent(); } + @Test + public void resetPasswordNewBrowserSession() throws IOException, MessagingException { + String username = "login-test"; + + loginPage.open(); + loginPage.resetPassword(); + + resetPasswordPage.assertCurrent(); + + resetPasswordPage.changePassword(username); + + resetPasswordPage.assertCurrent(); + + String sessionId = events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId(); + + assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); + + assertEquals(1, greenMail.getReceivedMessages().length); + + MimeMessage message = greenMail.getReceivedMessages()[0]; + + String body = (String) message.getContent(); + String changePasswordUrl = MailUtil.getLink(body); + + driver.manage().deleteAllCookies(); + + driver.navigate().to(changePasswordUrl.trim()); + + updatePasswordPage.assertCurrent(); + + updatePasswordPage.changePassword("resetPassword", "resetPassword"); + + events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).session(sessionId).detail(Details.USERNAME, username).assertEvent(); + + assertTrue(infoPage.isCurrent()); + assertEquals("Password updated", infoPage.getInfo()); + + loginPage.open(); + + assertTrue(loginPage.isCurrent()); + } + } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/InfoPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/InfoPage.java new file mode 100644 index 0000000000..516d6257a8 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/InfoPage.java @@ -0,0 +1,53 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2012, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.keycloak.testsuite.pages; + +import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.rule.WebResource; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +/** + * @author Stian Thorgersen + */ +public class InfoPage extends AbstractPage { + + @WebResource + protected OAuthClient oauth; + + @FindBy(className = "instruction") + private WebElement infoMessage; + + public String getInfo() { + return infoMessage.getText(); + } + + public boolean isCurrent() { + return driver.getPageSource().contains("kc-info-message"); + } + + @Override + public void open() { + throw new UnsupportedOperationException(); + } + +} From fb745d1daef4b92b6f038df497c5320375b1fb19 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 4 Mar 2015 15:19:00 +0100 Subject: [PATCH 4/4] Removed reference to auth-server.war in appliance docs --- docbook/reference/en/en-US/modules/server-installation.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docbook/reference/en/en-US/modules/server-installation.xml b/docbook/reference/en/en-US/modules/server-installation.xml index 35d231db30..bef369fc5d 100755 --- a/docbook/reference/en/en-US/modules/server-installation.xml +++ b/docbook/reference/en/en-US/modules/server-installation.xml @@ -36,8 +36,6 @@ keycloak-appliance-dist-all-&project.version;/ bin/ standalone.sh standalone.bat - standalone/deployments/ - auth-server.war/ standalone/configuration/ keycloak-server.json themes/