keycloak-uncached

Merge pull request #1774 from mposolda/1.6.x Migration

10/23/2015 7:41:42 PM

Details

diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java
index 60b0635..401cf74 100755
--- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java
+++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java
@@ -12,7 +12,7 @@ public interface JpaUpdaterProvider extends Provider {
 
     public String FIRST_VERSION = "1.0.0.Final";
 
-    public String LAST_VERSION = "1.6.0";
+    public String LAST_VERSION = "1.6.1";
 
     public String getCurrentVersionSql(String defaultSchema);
 
diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.1.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.1.xml
new file mode 100644
index 0000000..4e0129e
--- /dev/null
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.1.xml
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
+    <changeSet author="mposolda@redhat.com" id="1.6.1_from15">
+
+        <preConditions onFail="MARK_RAN" onFailMessage="Upgrading from 1.6.0 version. Skipped 1.6.1_from15 changeSet and marked as ran">
+            <not>
+                <changeSetExecuted id="1.6.0" author="mposolda@redhat.com" changeLogFile="META-INF/jpa-changelog-1.6.0.xml" />
+            </not>
+        </preConditions>
+
+        <addColumn tableName="REALM">
+            <column name="OFFLINE_SESSION_IDLE_TIMEOUT" type="INT" defaultValueNumeric="0"/>
+            <column name="REVOKE_REFRESH_TOKEN" type="BOOLEAN" defaultValueBoolean="false">
+                <constraints nullable="false"/>
+            </column>
+        </addColumn>
+
+        <addColumn tableName="KEYCLOAK_ROLE">
+            <column name="SCOPE_PARAM_REQUIRED" type="BOOLEAN" defaultValueBoolean="false">
+                <constraints nullable="false"/>
+            </column>
+        </addColumn>
+
+        <addColumn tableName="CLIENT">
+            <column name="ROOT_URL" type="VARCHAR(255)"/>
+            <column name="DESCRIPTION" type="VARCHAR(255)"/>
+        </addColumn>
+
+        <createTable tableName="OFFLINE_USER_SESSION">
+            <column name="USER_SESSION_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="USER_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="REALM_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="LAST_SESSION_REFRESH" type="INT"/>
+            <column name="OFFLINE_FLAG" type="VARCHAR(4)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="DATA" type="CLOB"/>
+        </createTable>
+
+        <createTable tableName="OFFLINE_CLIENT_SESSION">
+            <column name="CLIENT_SESSION_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="USER_SESSION_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="CLIENT_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="OFFLINE_FLAG" type="VARCHAR(4)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="TIMESTAMP" type="INT"/>
+            <column name="DATA" type="CLOB"/>
+        </createTable>
+
+        <addPrimaryKey columnNames="USER_SESSION_ID, OFFLINE_FLAG" constraintName="CONSTRAINT_OFFL_US_SES_PK2" tableName="OFFLINE_USER_SESSION"/>
+        <addPrimaryKey columnNames="CLIENT_SESSION_ID, OFFLINE_FLAG" constraintName="CONSTRAINT_OFFL_CL_SES_PK2" tableName="OFFLINE_CLIENT_SESSION"/>
+    </changeSet>
+
+    <!-- Just for the update from 1.6.0 -->
+    <changeSet author="mposolda@redhat.com" id="1.6.1_from16">
+
+        <preConditions onFail="MARK_RAN" onFailMessage="Upgrading from 1.5.0 or older version. Skipped 1.6.1_from16 changeSet and marked as ran">
+            <changeSetExecuted id="1.6.0" author="mposolda@redhat.com" changeLogFile="META-INF/jpa-changelog-1.6.0.xml" />
+        </preConditions>
+
+        <dropPrimaryKey constraintName="CONSTRAINT_OFFLINE_US_SES_PK" tableName="OFFLINE_USER_SESSION" />
+        <dropPrimaryKey constraintName="CONSTRAINT_OFFLINE_CL_SES_PK" tableName="OFFLINE_CLIENT_SESSION" />
+
+        <addColumn tableName="OFFLINE_USER_SESSION">
+            <column name="OFFLINE_FLAG" type="VARCHAR(4)">
+                <constraints nullable="false"/>
+            </column>
+        </addColumn>
+        <update tableName="OFFLINE_USER_SESSION">
+            <column name="OFFLINE_FLAG" value="1"/>
+        </update>
+        <dropColumn tableName="OFFLINE_USER_SESSION" columnName="OFFLINE" />
+
+        <addColumn tableName="OFFLINE_CLIENT_SESSION">
+            <column name="OFFLINE_FLAG" type="VARCHAR(4)">
+                <constraints nullable="false"/>
+            </column>
+        </addColumn>
+        <update tableName="OFFLINE_CLIENT_SESSION">
+            <column name="OFFLINE_FLAG" value="1"/>
+        </update>
+        <dropColumn tableName="OFFLINE_CLIENT_SESSION" columnName="OFFLINE" />
+
+        <addPrimaryKey columnNames="USER_SESSION_ID, OFFLINE_FLAG" constraintName="CONSTRAINT_OFFL_US_SES_PK2" tableName="OFFLINE_USER_SESSION"/>
+        <addPrimaryKey columnNames="CLIENT_SESSION_ID, OFFLINE_FLAG" constraintName="CONSTRAINT_OFFL_CL_SES_PK2" tableName="OFFLINE_CLIENT_SESSION"/>
+
+    </changeSet>
+
+    <changeSet author="mposolda@redhat.com" id="1.6.1">
+    </changeSet>
+
+</databaseChangeLog>
\ No newline at end of file
diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml
index 6cd96c6..3010118 100755
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml
@@ -9,5 +9,5 @@
     <include file="META-INF/jpa-changelog-1.3.0.xml"/>
     <include file="META-INF/jpa-changelog-1.4.0.xml"/>
     <include file="META-INF/jpa-changelog-1.5.0.xml"/>
-    <include file="META-INF/jpa-changelog-1.6.0.xml"/>
+    <include file="META-INF/jpa-changelog-1.6.1.xml"/>
 </databaseChangeLog>
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 cb5f2be..fccfce1 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
@@ -52,6 +52,8 @@ public interface LoginFormsProvider extends Provider {
 
     public LoginFormsProvider setClientSessionCode(String accessCode);
 
+    public LoginFormsProvider setClientSession(ClientSessionModel clientSession);
+
     public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String,RoleModel> resourceRolesRequested, List<ProtocolMapperModel> protocolMappers);
     public LoginFormsProvider setAccessRequest(String message);
 
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 7fc1bcd..6125d0b 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
@@ -47,6 +47,7 @@ import org.keycloak.login.freemarker.model.TotpBean;
 import org.keycloak.login.freemarker.model.UrlBean;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.Constants;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
@@ -138,7 +139,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
             case VERIFY_EMAIL:
                 try {
                     UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri());
-                    builder.queryParam("key", accessCode);
+                    builder.queryParam(OAuth2Constants.CODE, accessCode);
+                    builder.queryParam("key", clientSession.getNote(Constants.VERIFY_EMAIL_KEY));
 
                     String link = builder.build(realm.getName()).toString();
                     long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
@@ -532,6 +534,12 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
     }
 
     @Override
+    public LoginFormsProvider setClientSession(ClientSessionModel clientSession) {
+        this.clientSession = clientSession;
+        return this;
+    }
+
+    @Override
     public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested, List<ProtocolMapperModel> protocolMappersRequested) {
         this.realmRolesRequested = realmRolesRequested;
         this.resourceRolesRequested = resourceRolesRequested;
diff --git a/model/api/src/main/java/org/keycloak/models/Constants.java b/model/api/src/main/java/org/keycloak/models/Constants.java
index 43bdc7d..8977def 100755
--- a/model/api/src/main/java/org/keycloak/models/Constants.java
+++ b/model/api/src/main/java/org/keycloak/models/Constants.java
@@ -22,4 +22,6 @@ public interface Constants {
 
     // 30 days
     int DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT = 2592000;
+
+    public static final String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
 }
diff --git a/model/api/src/main/java/org/keycloak/models/utils/PostMigrationEvent.java b/model/api/src/main/java/org/keycloak/models/utils/PostMigrationEvent.java
new file mode 100644
index 0000000..513836f
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/utils/PostMigrationEvent.java
@@ -0,0 +1,11 @@
+package org.keycloak.models.utils;
+
+import org.keycloak.provider.ProviderEvent;
+
+/**
+ * Executed at startup after model migration is finished
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class PostMigrationEvent implements ProviderEvent {
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java
index 51a5239..cad8ce8 100644
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java
@@ -17,14 +17,14 @@ import javax.persistence.Table;
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
 @NamedQueries({
-        @NamedQuery(name="deleteClientSessionsByRealm", query="delete from PersistentClientSessionEntity sess where sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.realmId=:realmId)"),
-        @NamedQuery(name="deleteClientSessionsByClient", query="delete from PersistentClientSessionEntity sess where sess.clientId=:clientId"),
-        @NamedQuery(name="deleteClientSessionsByUser", query="delete from PersistentClientSessionEntity sess where sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.userId=:userId)"),
-        @NamedQuery(name="deleteClientSessionsByUserSession", query="delete from PersistentClientSessionEntity sess where sess.userSessionId=:userSessionId and sess.offline=:offline"),
+        @NamedQuery(name="deleteClientSessionsByRealm", query="delete from PersistentClientSessionEntity sess where sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.realmId = :realmId)"),
+        @NamedQuery(name="deleteClientSessionsByClient", query="delete from PersistentClientSessionEntity sess where sess.clientId = :clientId"),
+        @NamedQuery(name="deleteClientSessionsByUser", query="delete from PersistentClientSessionEntity sess where sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.userId = :userId)"),
+        @NamedQuery(name="deleteClientSessionsByUserSession", query="delete from PersistentClientSessionEntity sess where sess.userSessionId = :userSessionId and sess.offline = :offline"),
         @NamedQuery(name="deleteDetachedClientSessions", query="delete from PersistentClientSessionEntity sess where sess.userSessionId NOT IN (select u.userSessionId from PersistentUserSessionEntity u)"),
-        @NamedQuery(name="findClientSessionsByUserSession", query="select sess from PersistentClientSessionEntity sess where sess.userSessionId=:userSessionId and sess.offline=:offline"),
-        @NamedQuery(name="findClientSessionsByUserSessions", query="select sess from PersistentClientSessionEntity sess where sess.offline=:offline and sess.userSessionId IN (:userSessionIds) order by sess.userSessionId"),
-        @NamedQuery(name="updateClientSessionsTimestamps", query="update PersistentClientSessionEntity c set timestamp=:timestamp"),
+        @NamedQuery(name="findClientSessionsByUserSession", query="select sess from PersistentClientSessionEntity sess where sess.userSessionId=:userSessionId and sess.offline = :offline"),
+        @NamedQuery(name="findClientSessionsByUserSessions", query="select sess from PersistentClientSessionEntity sess where sess.offline = :offline and sess.userSessionId IN (:userSessionIds) order by sess.userSessionId"),
+        @NamedQuery(name="updateClientSessionsTimestamps", query="update PersistentClientSessionEntity c set timestamp = :timestamp"),
 })
 @Table(name="OFFLINE_CLIENT_SESSION")
 @Entity
@@ -45,8 +45,8 @@ public class PersistentClientSessionEntity {
     protected int timestamp;
 
     @Id
-    @Column(name = "OFFLINE")
-    protected boolean offline;
+    @Column(name = "OFFLINE_FLAG")
+    protected String offline;
 
     @Column(name="DATA")
     protected String data;
@@ -83,11 +83,11 @@ public class PersistentClientSessionEntity {
         this.timestamp = timestamp;
     }
 
-    public boolean isOffline() {
+    public String getOffline() {
         return offline;
     }
 
-    public void setOffline(boolean offline) {
+    public void setOffline(String offline) {
         this.offline = offline;
     }
 
@@ -103,12 +103,12 @@ public class PersistentClientSessionEntity {
 
         protected String clientSessionId;
 
-        protected boolean offline;
+        protected String offline;
 
         public Key() {
         }
 
-        public Key(String clientSessionId, boolean offline) {
+        public Key(String clientSessionId, String offline) {
             this.clientSessionId = clientSessionId;
             this.offline = offline;
         }
@@ -117,7 +117,7 @@ public class PersistentClientSessionEntity {
             return clientSessionId;
         }
 
-        public boolean isOffline() {
+        public String getOffline() {
             return offline;
         }
 
@@ -129,7 +129,7 @@ public class PersistentClientSessionEntity {
             Key key = (Key) o;
 
             if (this.clientSessionId != null ? !this.clientSessionId.equals(key.clientSessionId) : key.clientSessionId != null) return false;
-            if (offline != key.offline) return false;
+            if (this.offline != null ? !this.offline.equals(key.offline) : key.offline != null) return false;
 
             return true;
         }
@@ -137,7 +137,7 @@ public class PersistentClientSessionEntity {
         @Override
         public int hashCode() {
             int result = this.clientSessionId != null ? this.clientSessionId.hashCode() : 0;
-            result = 31 * result + (offline ? 1 : 0);
+            result = 31 * result + (this.offline != null ? this.offline.hashCode() : 0);
             return result;
         }
     }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java
index f56284c..abc581b 100644
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java
@@ -22,12 +22,12 @@ import org.keycloak.models.jpa.entities.UserEntity;
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
 @NamedQueries({
-        @NamedQuery(name="deleteUserSessionsByRealm", query="delete from PersistentUserSessionEntity sess where sess.realmId=:realmId"),
-        @NamedQuery(name="deleteUserSessionsByUser", query="delete from PersistentUserSessionEntity sess where sess.userId=:userId"),
+        @NamedQuery(name="deleteUserSessionsByRealm", query="delete from PersistentUserSessionEntity sess where sess.realmId = :realmId"),
+        @NamedQuery(name="deleteUserSessionsByUser", query="delete from PersistentUserSessionEntity sess where sess.userId = :userId"),
         @NamedQuery(name="deleteDetachedUserSessions", query="delete from PersistentUserSessionEntity sess where sess.userSessionId NOT IN (select c.userSessionId from PersistentClientSessionEntity c)"),
-        @NamedQuery(name="findUserSessionsCount", query="select count(sess) from PersistentUserSessionEntity sess where sess.offline=:offline"),
-        @NamedQuery(name="findUserSessions", query="select sess from PersistentUserSessionEntity sess where sess.offline=:offline order by sess.userSessionId"),
-        @NamedQuery(name="updateUserSessionsTimestamps", query="update PersistentUserSessionEntity c set lastSessionRefresh=:lastSessionRefresh"),
+        @NamedQuery(name="findUserSessionsCount", query="select count(sess) from PersistentUserSessionEntity sess where sess.offline = :offline"),
+        @NamedQuery(name="findUserSessions", query="select sess from PersistentUserSessionEntity sess where sess.offline = :offline order by sess.userSessionId"),
+        @NamedQuery(name="updateUserSessionsTimestamps", query="update PersistentUserSessionEntity c set lastSessionRefresh = :lastSessionRefresh"),
 
 })
 @Table(name="OFFLINE_USER_SESSION")
@@ -49,8 +49,8 @@ public class PersistentUserSessionEntity {
     protected int lastSessionRefresh;
 
     @Id
-    @Column(name = "OFFLINE")
-    protected boolean offline;
+    @Column(name = "OFFLINE_FLAG")
+    protected String offline;
 
     @Column(name="DATA")
     protected String data;
@@ -87,11 +87,11 @@ public class PersistentUserSessionEntity {
         this.lastSessionRefresh = lastSessionRefresh;
     }
 
-    public boolean isOffline() {
+    public String getOffline() {
         return offline;
     }
 
-    public void setOffline(boolean offline) {
+    public void setOffline(String offline) {
         this.offline = offline;
     }
 
@@ -107,12 +107,12 @@ public class PersistentUserSessionEntity {
 
         protected String userSessionId;
 
-        protected boolean offline;
+        protected String offline;
 
         public Key() {
         }
 
-        public Key(String userSessionId, boolean offline) {
+        public Key(String userSessionId, String offline) {
             this.userSessionId = userSessionId;
             this.offline = offline;
         }
@@ -121,7 +121,7 @@ public class PersistentUserSessionEntity {
             return userSessionId;
         }
 
-        public boolean isOffline() {
+        public String getOffline() {
             return offline;
         }
 
@@ -133,7 +133,7 @@ public class PersistentUserSessionEntity {
             Key key = (Key) o;
 
             if (this.userSessionId != null ? !this.userSessionId.equals(key.userSessionId) : key.userSessionId != null) return false;
-            if (offline != key.offline) return false;
+            if (this.offline != null ? !this.offline.equals(key.offline) : key.offline != null) return false;
 
             return true;
         }
@@ -141,7 +141,7 @@ public class PersistentUserSessionEntity {
         @Override
         public int hashCode() {
             int result = this.userSessionId != null ? this.userSessionId.hashCode() : 0;
-            result = 31 * result + (offline ? 1 : 0);
+            result = 31 * result + (this.offline != null ? this.offline.hashCode() : 0);
             return result;
         }
     }
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
index 1d7c279..382d01f 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
@@ -5,9 +5,11 @@ import org.infinispan.Version;
 import org.jboss.logging.Logger;
 import org.keycloak.Config;
 import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.models.KeycloakSessionTask;
+import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserSessionProvider;
 import org.keycloak.models.UserSessionProviderFactory;
 import org.keycloak.models.session.UserSessionPersisterProvider;
@@ -19,6 +21,9 @@ import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
 import org.keycloak.models.sessions.infinispan.initializer.InfinispanUserSessionInitializer;
 import org.keycloak.models.sessions.infinispan.initializer.OfflineUserSessionLoader;
 import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.models.utils.PostMigrationEvent;
+import org.keycloak.provider.ProviderEvent;
+import org.keycloak.provider.ProviderEventListener;
 
 /**
  * Uses Infinispan to store user sessions. On EAP 6.4 (Infinispan 5.2) map reduce is not supported for local caches as a work around
@@ -68,13 +73,20 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
         });
 
         // Max count of worker errors. Initialization will end with exception when this number is reached
-        int maxErrors = config.getInt("maxErrors", 20);
+        final int maxErrors = config.getInt("maxErrors", 20);
 
         // Count of sessions to be computed in each segment
-        int sessionsPerSegment = config.getInt("sessionsPerSegment", 100);
+        final int sessionsPerSegment = config.getInt("sessionsPerSegment", 100);
 
-        // TODO: Possibility to run this asynchronously to not block start time
-        loadPersistentSessions(factory, maxErrors, sessionsPerSegment);
+        factory.register(new ProviderEventListener() {
+
+            @Override
+            public void onEvent(ProviderEvent event) {
+                if (event instanceof PostMigrationEvent) {
+                    loadPersistentSessions(factory, maxErrors, sessionsPerSegment);
+                }
+            }
+        });
     }
 
 
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
index 01ddcff..7fc77a8 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
@@ -8,9 +8,12 @@ import org.keycloak.authentication.RequiredActionProvider;
 import org.keycloak.events.Details;
 import org.keycloak.events.EventType;
 import org.keycloak.login.LoginFormsProvider;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.Constants;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.HmacOTP;
 import org.keycloak.services.resources.LoginActionsService;
 import org.keycloak.services.validation.Validation;
 
@@ -44,8 +47,11 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
         context.getEvent().clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, context.getUser().getEmail()).success();
         LoginActionsService.createActionCookie(context.getRealm(), context.getUriInfo(), context.getConnection(), context.getUserSession().getId());
 
+        setupKey(context.getClientSession());
+
         LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class)
                 .setClientSessionCode(context.generateCode())
+                .setClientSession(context.getClientSession())
                 .setUser(context.getUser());
         Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.VERIFY_EMAIL);
         context.challenge(challenge);
@@ -87,4 +93,9 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
     public String getId() {
         return UserModel.RequiredAction.VERIFY_EMAIL.name();
     }
+
+    public static void setupKey(ClientSessionModel clientSession) {
+        String secret = HmacOTP.generateSecret(10);
+        clientSession.setNote(Constants.VERIFY_EMAIL_KEY, secret);
+    }
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index 83b8d6e..a07999d 100755
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -11,6 +11,7 @@ import org.keycloak.migration.MigrationModelManager;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.PostMigrationEvent;
 import org.keycloak.offlineconfig.AdminRecovery;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.services.DefaultKeycloakSessionFactory;
@@ -83,6 +84,8 @@ public class KeycloakApplication extends Application {
         setupDefaultRealm(context.getContextPath());
 
         migrateModel();
+        sessionFactory.publish(new PostMigrationEvent());
+
         new ExportImportManager().checkExportImport(this.sessionFactory, context.getContextPath());
         importRealms(context);
 
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 46bc553..20de78b 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -23,6 +23,8 @@ package org.keycloak.services.resources;
 
 import org.jboss.logging.Logger;
 import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.authentication.requiredactions.VerifyEmail;
 import org.keycloak.common.ClientConnection;
 import org.keycloak.OAuth2Constants;
 import org.keycloak.authentication.AuthenticationProcessor;
@@ -49,6 +51,7 @@ import org.keycloak.models.UserModel;
 import org.keycloak.models.UserModel.RequiredAction;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.utils.FormMessage;
+import org.keycloak.models.utils.HmacOTP;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.protocol.LoginProtocol;
 import org.keycloak.protocol.RestartLoginCookie;
@@ -533,7 +536,7 @@ public class LoginActionsService {
         event.event(EventType.VERIFY_EMAIL);
         if (key != null) {
             Checks checks = new Checks();
-            if (!checks.verifyCode(key, ClientSessionModel.Action.REQUIRED_ACTIONS.name())) {
+            if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name())) {
                 return checks.response;
             }
             ClientSessionCode accessCode = checks.clientCode;
@@ -547,11 +550,21 @@ public class LoginActionsService {
             UserSessionModel userSession = clientSession.getUserSession();
             UserModel user = userSession.getUser();
             initEvent(clientSession);
+            event.event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail());
+
+            String keyFromSession = clientSession.getNote(Constants.VERIFY_EMAIL_KEY);
+            clientSession.removeNote(Constants.VERIFY_EMAIL_KEY);
+            if (!key.equals(keyFromSession)) {
+                logger.error("Invalid key for email verification");
+                event.error(Errors.INVALID_USER_CREDENTIALS);
+                throw new WebApplicationException(ErrorPage.error(session, Messages.INVALID_CODE));
+            }
+
             user.setEmailVerified(true);
 
             user.removeRequiredAction(RequiredAction.VERIFY_EMAIL);
 
-            event.event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail()).success();
+            event.success();
 
             String actionCookieValue = getActionCookie();
             if (actionCookieValue == null || !actionCookieValue.equals(userSession.getId())) {
@@ -576,8 +589,11 @@ public class LoginActionsService {
 
             createActionCookie(realm, uriInfo, clientConnection, userSession.getId());
 
+            VerifyEmail.setupKey(clientSession);
+
             return session.getProvider(LoginFormsProvider.class)
                     .setClientSessionCode(accessCode.getCode())
+                    .setClientSession(clientSession)
                     .setUser(userSession.getUser())
                     .createResponse(RequiredAction.VERIFY_EMAIL);
         }
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 a9c0c58..c7f075f 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
@@ -26,7 +26,9 @@ import org.junit.Before;
 import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
+import org.keycloak.common.util.KeycloakUriBuilder;
 import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
 import org.keycloak.events.Event;
 import org.keycloak.events.EventType;
 import org.keycloak.models.RealmModel;
@@ -130,7 +132,7 @@ public class RequiredActionEmailVerificationTest {
 
         String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
 
-        Assert.assertEquals(mailCodeId, verificationUrl.split("key=")[1].split("\\.")[1]);
+        Assert.assertEquals(mailCodeId, verificationUrl.split("code=")[1].split("\\&")[0].split("\\.")[1]);
 
         driver.navigate().to(verificationUrl.trim());
 
@@ -223,7 +225,7 @@ public class RequiredActionEmailVerificationTest {
 
         String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
 
-        Assert.assertEquals(mailCodeId, verificationUrl.split("key=")[1].split("\\.")[1]);
+        Assert.assertEquals(mailCodeId, verificationUrl.split("code=")[1].split("\\&")[0].split("\\.")[1]);
 
         driver.manage().deleteAllCookies();
 
@@ -238,6 +240,42 @@ public class RequiredActionEmailVerificationTest {
 
         assertTrue(loginPage.isCurrent());
     }
+
+
+    @Test
+    public void verifyInvalidKeyOrCode() throws IOException, MessagingException {
+        loginPage.open();
+        loginPage.login("test-user@localhost", "password");
+
+        Assert.assertTrue(verifyEmailPage.isCurrent());
+        String resendEmailLink = verifyEmailPage.getResendEmailLink();
+        String keyInsteadCodeURL = resendEmailLink.replace("code=", "key=");
+
+        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);
+
+        driver.navigate().to(keyInsteadCodeURL);
+
+        events.expectRequiredAction(EventType.VERIFY_EMAIL_ERROR)
+                .error(Errors.INVALID_CODE)
+                .client((String)null)
+                .user((String)null)
+                .session((String)null)
+                .clearDetails()
+                .assertEvent();
+
+        String badKeyURL = KeycloakUriBuilder.fromUri(resendEmailLink).queryParam("key", "foo").build().toString();
+        driver.navigate().to(badKeyURL);
+
+        events.expectRequiredAction(EventType.VERIFY_EMAIL_ERROR)
+                .error(Errors.INVALID_USER_CREDENTIALS)
+                .session(sessionId)
+                .detail("email", "test-user@localhost")
+                .detail(Details.CODE_ID, mailCodeId)
+                .assertEvent();
+    }
     
     private String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException {
     	Multipart multipart = (Multipart) message.getContent();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/VerifyEmailPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/VerifyEmailPage.java
index cfcfbb4..9968ce1 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/VerifyEmailPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/VerifyEmailPage.java
@@ -50,4 +50,8 @@ public class VerifyEmailPage extends AbstractPage {
         resendEmailLink.click();
     }
 
+    public String getResendEmailLink() {
+        return resendEmailLink.getAttribute("href");
+    }
+
 }