keycloak-aplcache

merge conflicts

3/4/2015 7:27:44 PM

Changes

Details

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 f510922..f1a35c9 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 @@
             <column name="PROTOCOL_MAPPER_NAME" type="VARCHAR(255)">
                 <constraints nullable="false"/>
             </column>
-            <column name="APPLIED_BY_DEFAULT" type="BOOLEAN(1)"/>
-            <column name="CONSENT_REQUIRED" type="BOOLEAN(1)"/>
+            <column name="APPLIED_BY_DEFAULT" type="BOOLEAN" defaultValueBoolean="false"/>
+            <column name="CONSENT_REQUIRED" type="BOOLEAN" defaultValueBoolean="false"/>
             <column name="CONSENT_TEXT" type="VARCHAR(255)"/>
             <column name="CLIENT_ID" type="VARCHAR(36)">
                 <constraints nullable="false"/>
@@ -46,13 +46,13 @@
             <column name="INTERNAL_ID" type="VARCHAR(36)">
                 <constraints nullable="false"/>
             </column>
-            <column name="ENABLED" type="BOOLEAN(1)"/>
+            <column name="ENABLED" type="BOOLEAN" defaultValueBoolean="false"/>
             <column name="PROVIDER_NONIMAL_ID" type="VARCHAR(255)"/>
             <column name="PROVIDER_NAME" type="VARCHAR(255)"/>
             <column name="PROVIDER_ID" type="VARCHAR(255)"/>
-            <column name="UPDATE_PROFILE_FIRST_LOGIN" type="BOOLEAN(1)"/>
-            <column name="STORE_TOKEN" type="BOOLEAN(1)"/>
-            <column name="AUTHENTICATE_BY_DEFAULT" type="BOOLEAN(1)"/>
+            <column name="UPDATE_PROFILE_FIRST_LOGIN" type="BOOLEAN" defaultValueBoolean="false"/>
+            <column name="STORE_TOKEN" type="BOOLEAN" defaultValueBoolean="false"/>
+            <column name="AUTHENTICATE_BY_DEFAULT" type="BOOLEAN" defaultValueBoolean="false"/>
             <column name="REALM_ID" type="VARCHAR(36)"/>
         </createTable>
         <createTable tableName="IDENTITY_PROVIDER_CONFIG">
@@ -71,7 +71,7 @@
             <column name="IDENTITY_PROVIDER_ID" type="VARCHAR(36)">
                 <constraints nullable="false"/>
             </column>
-            <column name="RETRIEVE_TOKEN" type="BOOLEAN(1)"/>
+            <column name="RETRIEVE_TOKEN" type="BOOLEAN" defaultValueBoolean="false"/>
         </createTable>
         <addColumn tableName="CLIENT">
             <column name="FRONTCHANNEL_LOGOUT" type="BOOLEAN" defaultValueBoolean="false"/>
@@ -90,5 +90,9 @@
         <addForeignKeyConstraint baseColumnNames="CLIENT_ID" baseTableName="CLIENT_IDENTITY_PROVIDER_MAPPING" constraintName="FK_56ELWNIBJI49AVXSRTUF6XJ23" referencedColumnNames="ID" referencedTableName="CLIENT"/>
         <addUniqueConstraint columnNames="PROVIDER_NONIMAL_ID" constraintName="UK_2DAELWNIBJI49AVXSRTUF6XJ33" tableName="IDENTITY_PROVIDER"/>
         <addUniqueConstraint columnNames="IDENTITY_PROVIDER_ID,CLIENT_ID" constraintName="UK_7CAELWNIBJI49AVXSRTUF6XJ12" tableName="CLIENT_IDENTITY_PROVIDER_MAPPING"/>
+
+        <addColumn tableName="REALM">
+            <column name="LOGIN_LIFESPAN" type="INT"/>
+        </addColumn>
     </changeSet>
 </databaseChangeLog>
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 ae57f06..dce6df6 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<String> getDefaultRoles() {
         return defaultRoles;
     }
diff --git a/docbook/reference/en/en-US/modules/server-installation.xml b/docbook/reference/en/en-US/modules/server-installation.xml
index 35d231d..bef369f 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/
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 140cbbe..7c5090d 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 c322bf2..2f30127 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
@@ -93,6 +93,25 @@
                     <span tooltip-placement="right" tooltip="Max time an application or oauth client has to finish the access token protocol.  This should normally be 1 minute." class="fa fa-info-circle"></span>
                 </div>
                 <div class="form-group input-select">
+                    <label class="col-sm-2 control-label" for="accessCodeLifespanLogin" class="two-lines">Login lifespan</label>
+                    <div class="col-sm-5">
+                        <div class="row">
+                            <div class="col-sm-4">
+                                <input class="form-control" type="number" required min="1" max="31536000" data-ng-model="realm.accessCodeLifespanLogin" id="accessCodeLifespanLogin" name="accessCodeLifespanLogin">
+                            </div>
+                            <div class="col-sm-4 select-kc">
+                                <select name="accessCodeLifespanLoginUnit" data-ng-model="realm.accessCodeLifespanLoginUnit">
+                                    <option data-ng-selected="!realm.accessCodeLifespanLoginUnit">Seconds</option>
+                                    <option>Minutes</option>
+                                    <option>Hours</option>
+                                    <option>Days</option>
+                                </select>
+                            </div>
+                        </div>
+                    </div>
+                    <span tooltip-placement="right" tooltip="Max time a user has to complete a login.  This is recommended to be relatively long.  30 minutes or more." class="fa fa-info-circle"></span>
+                </div>
+                <div class="form-group input-select">
                     <label class="col-sm-2 control-label" for="accessCodeLifespanUserAction" class="two-lines">Login user action lifespan</label>
                     <div class="col-sm-5">
                         <div class="row">
@@ -109,7 +128,7 @@
                             </div>
                         </div>
                     </div>
-                    <span tooltip-placement="right" tooltip="Max time a user has to complete a login and perform actions like update password or configure totp.  This is recommended to be relatively long.  5 minutes or more." class="fa fa-info-circle"></span>
+                    <span tooltip-placement="right" tooltip="Max time a user has to complete login related actions like update password or configure totp.  This is recommended to be relatively long.  5 minutes or more." class="fa fa-info-circle"></span>
                 </div>
             </fieldset>
             <div class="pull-right form-actions" data-ng-show="access.manageRealm">
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 0000000..d303e5b
--- /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">
+    <div id="kc-info-message">
+        <p class="instruction">${message.summary}</p>
+        <#if client.baseUrl??>
+        <p><a href="${client.baseUrl}">${rb.backToApplication}</a></p>
+        </#if>
+    </div>
+    </#if>
+</@layout.registrationLayout>
\ 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 950e0b7..2f43473 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
@@ -25,6 +25,7 @@ rememberMe=Remember me
 passwordConfirm=Confirm password
 passwordNew=New Password
 passwordNewConfirm=New Password confirmation
+passwordUpdated=Password updated
 cancel=Cancel
 accept=Accept
 submit=Submit
@@ -90,6 +91,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...
@@ -111,6 +113,7 @@ errorHeader=Error!
 
 emailForgotHeader=Forgot Your Password?
 backToLogin=&laquo; Back to Login
+backToApplication=&laquo; 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 2b0cd23..e34b0ef 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 45f3adb..f9a9c48 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 44a7585..f8b85c6 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<RoleModel> 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<String, String> 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/model/ClientBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ClientBean.java
index 4507762..fcd9e11 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/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 02e20ec..785b186 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/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
index 47a97a8..17792e5 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/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java
index d002fd0..4212e3b 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/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index c90afb5..d0963df 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 0000000..e3e30dd
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 a986de5..0bfa82b 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/entities/CachedRealm.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
index 12cb2c5..8089e4e 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/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 9ade50f..57013cf 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
@@ -301,6 +301,18 @@ public class RealmAdapter implements RealmModel {
     }
 
     @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();
         return cached.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 258b88f..9a4358b 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/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index f056449..9c0e751 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
@@ -363,6 +363,17 @@ public class RealmAdapter implements RealmModel {
     }
 
     @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/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 c149e93..a51f801 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<MongoRealmEntity> impleme
         updateRealm();
     }
 
-
-
     @Override
     public int getAccessCodeLifespan() {
         return realm.getAccessCodeLifespan();
@@ -320,6 +318,17 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
     }
 
     @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 96e7f85..42dc3aa 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<String, String> 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 169a92d..dcae8e2 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 cd5eef1..41081c0 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<ClientSessionEntity> 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 6ddebb9..6630855 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/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index d02ad2b..25fce1f 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/managers/ClientSessionCode.java b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
index 24861f3..9e52c97 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 f3ae31b..f62bd5c 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);
@@ -272,7 +288,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);
@@ -689,7 +708,7 @@ public class LoginActionsService {
                                    final MultivaluedMap<String, String> 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;
@@ -723,7 +742,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);
     }
 
@@ -746,7 +775,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 {
@@ -759,6 +795,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())
@@ -776,7 +814,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);
@@ -863,11 +900,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 0f2b13f..1d66651 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
@@ -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/admin/RealmTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java
index 6626b60..5dd37bc 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 a1ef54c..75a83f4 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
@@ -199,6 +199,36 @@ public class LoginTest {
     }
 
     @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";
         driver.navigate().to(loginFormUrl);
@@ -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 580ba2a..45795da 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/forms/ResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
index 20926f4..7582e20 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
@@ -104,6 +107,9 @@ public class ResetPasswordTest {
     protected ErrorPage errorPage;
 
     @WebResource
+    protected InfoPage infoPage;
+
+    @WebResource
     protected LoginPasswordResetPage resetPasswordPage;
 
     @WebResource
@@ -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/model/UserSessionProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
index db5d370..228416d 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
@@ -294,6 +294,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 7ecd292..8048ed0 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);
 
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 0000000..516d625
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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();
+    }
+
+}