keycloak-aplcache

Changes

Details

diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java
index 998cbeb..f59be47 100644
--- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java
@@ -23,28 +23,23 @@ import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
 import org.infinispan.Cache;
 import org.infinispan.client.hotrod.RemoteCache;
 import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
-import org.infinispan.client.hotrod.annotation.ClientCacheEntryExpired;
 import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified;
 import org.infinispan.client.hotrod.annotation.ClientCacheEntryRemoved;
 import org.infinispan.client.hotrod.annotation.ClientListener;
 import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
-import org.infinispan.client.hotrod.event.ClientCacheEntryExpiredEvent;
 import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
 import org.infinispan.client.hotrod.event.ClientCacheEntryRemovedEvent;
 import org.infinispan.context.Flag;
 import org.infinispan.notifications.Listener;
 import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
-import org.infinispan.notifications.cachelistener.annotation.CacheEntryExpired;
 import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified;
 import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved;
 import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent;
-import org.infinispan.notifications.cachelistener.event.CacheEntryExpiredEvent;
 import org.infinispan.notifications.cachelistener.event.CacheEntryModifiedEvent;
 import org.infinispan.notifications.cachelistener.event.CacheEntryRemovedEvent;
 import org.infinispan.persistence.remote.RemoteStore;
@@ -52,8 +47,7 @@ import org.jboss.logging.Logger;
 import org.keycloak.cluster.ClusterEvent;
 import org.keycloak.cluster.ClusterListener;
 import org.keycloak.cluster.ClusterProvider;
-import org.keycloak.common.util.MultivaluedHashMap;
-
+import org.keycloak.common.util.ConcurrentMultivaluedHashMap;
 /**
  * Impl for sending infinispan messages across cluster and listening to them
  *
@@ -63,7 +57,7 @@ public class InfinispanNotificationsManager {
 
     protected static final Logger logger = Logger.getLogger(InfinispanNotificationsManager.class);
 
-    private final MultivaluedHashMap<String, ClusterListener> listeners = new MultivaluedHashMap<>();
+    private final ConcurrentMultivaluedHashMap<String, ClusterListener> listeners = new ConcurrentMultivaluedHashMap<>();
 
     private final ConcurrentMap<String, TaskCallback> taskCallbacks = new ConcurrentHashMap<>();
 
@@ -132,8 +126,10 @@ public class InfinispanNotificationsManager {
         wrappedEvent.setSender(myAddress);
         wrappedEvent.setSenderSite(mySite);
 
+        String eventKey = UUID.randomUUID().toString();
+
         if (logger.isTraceEnabled()) {
-            logger.tracef("Sending event: %s", event);
+            logger.tracef("Sending event with key %s: %s", eventKey, event);
         }
 
         Flag[] flags = dcNotify == ClusterProvider.DCNotify.LOCAL_DC_ONLY
@@ -142,7 +138,7 @@ public class InfinispanNotificationsManager {
 
         // Put the value to the cache to notify listeners on all the nodes
         workCache.getAdvancedCache().withFlags(flags)
-                .put(UUID.randomUUID().toString(), wrappedEvent, 120, TimeUnit.SECONDS);
+                .put(eventKey, wrappedEvent, 120, TimeUnit.SECONDS);
     }
 
 
@@ -208,6 +204,9 @@ public class InfinispanNotificationsManager {
 
     private void eventReceived(String key, Serializable obj) {
         if (!(obj instanceof WrapperClusterEvent)) {
+            if (obj == null) {
+                logger.warnf("Event object wasn't available in remote cache after event was received. Event key: %s", key);
+            }
             return;
         }
 
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStore.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStore.java
index b285290..c50bcf1 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStore.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStore.java
@@ -90,7 +90,7 @@ public class LastSessionRefreshStore {
         LastSessionRefreshEvent event = new LastSessionRefreshEvent(refreshesToSend);
 
         if (logger.isDebugEnabled()) {
-            logger.debugf("Sending lastSessionRefreshes: %s", event.getLastSessionRefreshes().toString());
+            logger.debugf("Sending lastSessionRefreshes for key '%s'. Refreshes: %s", eventKey, event.getLastSessionRefreshes().toString());
         }
 
         // Don't notify local DC about the lastSessionRefreshes. They were processed here already
diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheClientListenersTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheClientListenersTest.java
new file mode 100644
index 0000000..f18d8d3
--- /dev/null
+++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheClientListenersTest.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.cluster.infinispan;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.infinispan.Cache;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
+import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified;
+import org.infinispan.client.hotrod.annotation.ClientListener;
+import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
+import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
+import org.infinispan.configuration.cache.Configuration;
+import org.infinispan.configuration.cache.ConfigurationBuilder;
+import org.infinispan.configuration.global.GlobalConfigurationBuilder;
+import org.infinispan.manager.DefaultCacheManager;
+import org.infinispan.manager.EmbeddedCacheManager;
+import org.infinispan.persistence.manager.PersistenceManager;
+import org.infinispan.persistence.remote.RemoteStore;
+import org.infinispan.persistence.remote.configuration.ExhaustedAction;
+import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
+
+/**
+ * Test that hotrod ClientListeners are correctly executed as expected
+ *
+ * STEPS TO REPRODUCE:
+ * - Unzip infinispan-server-8.2.6.Final to some locations ISPN1 and ISPN2
+ *
+ * - Edit both ISPN1/standalone/configuration/clustered.xml and ISPN2/standalone/configuration/clustered.xml . Configure cache in container "clustered"
+ *
+ * 		<replicated-cache-configuration name="sessions-cfg" mode="ASYNC" start="EAGER" batching="false">
+            <transaction mode="NON_XA" locking="PESSIMISTIC"/>
+        </replicated-cache-configuration>
+
+        <replicated-cache name="work" configuration="sessions-cfg" />
+
+    - Run server1
+ ./standalone.sh -c clustered.xml -Djava.net.preferIPv4Stack=true -Djboss.socket.binding.port-offset=1010 -Djboss.default.multicast.address=234.56.78.99 -Djboss.node.name=cache-server
+
+    - Run server2
+ ./standalone.sh -c clustered.xml -Djava.net.preferIPv4Stack=true -Djboss.socket.binding.port-offset=2010 -Djboss.default.multicast.address=234.56.78.99 -Djboss.node.name=cache-server-dc-2
+
+    - Run this test as main class from IDE
+ *
+ *
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ConcurrencyJDGRemoteCacheClientListenersTest {
+
+    // Helper map to track if listeners were executed
+    private static Map<String, EntryInfo> state = new HashMap<>();
+
+    private static AtomicInteger totalListenerCalls = new AtomicInteger(0);
+
+    private static AtomicInteger totalErrors = new AtomicInteger(0);
+
+
+    public static void main(String[] args) throws Exception {
+        // Init map somehow
+        for (int i=0 ; i<1000 ; i++) {
+            String key = "key-" + i;
+            EntryInfo entryInfo = new EntryInfo();
+            entryInfo.val.set(i);
+            state.put(key, entryInfo);
+        }
+
+        // Create caches, listeners and finally worker threads
+        Worker worker1 = createWorker(1);
+        Worker worker2 = createWorker(2);
+
+        // Note "run", so it's not executed asynchronously here!!!
+        worker1.run();
+
+//
+//        // Start and join workers
+//        worker1.start();
+//        worker2.start();
+//
+//        worker1.join();
+//        worker2.join();
+
+        // Output
+        for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
+            System.out.println(entry.getKey() + ":::" + entry.getValue());
+        }
+
+        System.out.println("totalListeners: " + totalListenerCalls.get() + ", totalErrors: " + totalErrors.get());
+
+
+        // Assert that ClientListener was able to read the value and save it into EntryInfo
+        try {
+            for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
+                EntryInfo info = entry.getValue();
+                Assert.assertEquals(info.val.get(), info.dc1Created.get());
+                Assert.assertEquals(info.val.get(), info.dc2Created.get());
+                Assert.assertEquals(info.val.get() * 2, info.dc1Updated.get());
+                Assert.assertEquals(info.val.get() * 2, info.dc2Updated.get());
+                worker1.cache.remove(entry.getKey());
+            }
+        } finally {
+            // Finish JVM
+            worker1.cache.getCacheManager().stop();
+            worker2.cache.getCacheManager().stop();
+        }
+    }
+
+    private static Worker createWorker(int threadId) {
+        EmbeddedCacheManager manager = createManager(threadId);
+        Cache<String, Integer> cache = manager.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
+
+        System.out.println("Retrieved cache: " + threadId);
+
+        RemoteStore remoteStore = cache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class).iterator().next();
+        HotRodListener listener = new HotRodListener(cache, threadId);
+        remoteStore.getRemoteCache().addClientListener(listener);
+
+        return new Worker(cache, threadId);
+    }
+
+    private static EmbeddedCacheManager createManager(int threadId) {
+        System.setProperty("java.net.preferIPv4Stack", "true");
+        System.setProperty("jgroups.tcp.port", "53715");
+        GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
+
+        boolean clustered = false;
+        boolean async = false;
+        boolean allowDuplicateJMXDomains = true;
+
+        if (clustered) {
+            gcb = gcb.clusteredDefault();
+            gcb.transport().clusterName("test-clustering");
+        }
+
+        gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
+
+        EmbeddedCacheManager cacheManager = new DefaultCacheManager(gcb.build());
+
+        Configuration invalidationCacheConfiguration = getCacheBackedByRemoteStore(threadId);
+
+        cacheManager.defineConfiguration(InfinispanConnectionProvider.WORK_CACHE_NAME, invalidationCacheConfiguration);
+        return cacheManager;
+
+    }
+
+    private static Configuration getCacheBackedByRemoteStore(int threadId) {
+        ConfigurationBuilder cacheConfigBuilder = new ConfigurationBuilder();
+
+        int port = threadId==1 ? 12232 : 13232;
+        //int port = 12232;
+
+        return cacheConfigBuilder.persistence().addStore(RemoteStoreConfigurationBuilder.class)
+                .fetchPersistentState(false)
+                .ignoreModifications(false)
+                .purgeOnStartup(false)
+                .preload(false)
+                .shared(true)
+                .remoteCacheName(InfinispanConnectionProvider.WORK_CACHE_NAME)
+                .rawValues(true)
+                .forceReturnValues(false)
+                .addServer()
+                .host("localhost")
+                .port(port)
+                .connectionPool()
+                .maxActive(20)
+                .exhaustedAction(ExhaustedAction.CREATE_NEW)
+                .async()
+                .   enabled(false).build();
+    }
+
+
+    @ClientListener
+    public static class HotRodListener {
+
+        private final RemoteCache<String, Integer> remoteCache;
+        private final int threadId;
+
+        public HotRodListener(Cache<String, Integer> cache, int threadId) {
+            this.remoteCache = InfinispanUtil.getRemoteCache(cache);
+            this.threadId = threadId;
+        }
+
+        //private AtomicInteger listenerCount = new AtomicInteger(0);
+
+        @ClientCacheEntryCreated
+        public void created(ClientCacheEntryCreatedEvent event) {
+            String cacheKey = (String) event.getKey();
+            event(cacheKey, true);
+
+        }
+
+
+        @ClientCacheEntryModified
+        public void updated(ClientCacheEntryModifiedEvent event) {
+            String cacheKey = (String) event.getKey();
+            event(cacheKey, false);
+        }
+
+
+        private void event(String cacheKey, boolean created) {
+            EntryInfo entryInfo = state.get(cacheKey);
+            entryInfo.successfulListenerWrites.incrementAndGet();
+
+            totalListenerCalls.incrementAndGet();
+
+            Integer val = remoteCache.get(cacheKey);
+            if (val != null) {
+                AtomicInteger dcVal;
+                if (created) {
+                    dcVal = threadId == 1 ? entryInfo.dc1Created : entryInfo.dc2Created;
+                } else {
+                    dcVal = threadId == 1 ? entryInfo.dc1Updated : entryInfo.dc2Updated;
+                }
+                dcVal.set(val);
+            } else {
+                System.err.println("NOT A VALUE FOR KEY: " + cacheKey);
+                totalErrors.incrementAndGet();
+            }
+        }
+
+    }
+
+
+    private static class Worker extends Thread {
+
+        private final Cache<String, Integer> cache;
+
+        private final int myThreadId;
+
+        private Worker(Cache<String, Integer> cache, int myThreadId) {
+            this.cache = cache;
+            this.myThreadId = myThreadId;
+        }
+
+        @Override
+        public void run() {
+            for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
+                String cacheKey = entry.getKey();
+                Integer value = entry.getValue().val.get();
+
+                this.cache.put(cacheKey, value);
+            }
+
+            System.out.println("Worker creating finished: " + myThreadId);
+
+            for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
+                String cacheKey = entry.getKey();
+                Integer value = entry.getValue().val.get() * 2;
+
+                this.cache.replace(cacheKey, value);
+            }
+
+            System.out.println("Worker updating finished: " + myThreadId);
+        }
+
+    }
+
+
+    public static class EntryInfo {
+        AtomicInteger val = new AtomicInteger();
+        AtomicInteger successfulListenerWrites = new AtomicInteger(0);
+        AtomicInteger dc1Created = new AtomicInteger();
+        AtomicInteger dc2Created = new AtomicInteger();
+        AtomicInteger dc1Updated = new AtomicInteger();
+        AtomicInteger dc2Updated = new AtomicInteger();
+
+        @Override
+        public String toString() {
+            return String.format("val: %d, successfulListenerWrites: %d, dc1Created: %d, dc2Created: %d, dc1Updated: %d, dc2Updated: %d", val.get(), successfulListenerWrites.get(),
+                    dc1Created.get(), dc2Created.get(), dc1Updated.get(), dc2Updated.get());
+        }
+    }
+}
diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java
index d86e6f8..df1b80e 100644
--- a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java
+++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java
@@ -43,11 +43,12 @@ import org.junit.Ignore;
 import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
 
 /**
- * Test concurrency for remoteStore (backed by HotRod RemoteCaches) against external JDG
+ * Test concurrency for remoteStore (backed by HotRod RemoteCaches) against external JDG. Especially tests "putIfAbsent" contract.
+ *
+ * Steps: {@see ConcurrencyJDGRemoteCacheClientListenersTest}
  *
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
-@Ignore
 public class ConcurrencyJDGRemoteCacheTest {
 
     private static Map<String, EntryInfo> state = new HashMap<>();
@@ -122,8 +123,8 @@ public class ConcurrencyJDGRemoteCacheTest {
     private static Configuration getCacheBackedByRemoteStore(int threadId) {
         ConfigurationBuilder cacheConfigBuilder = new ConfigurationBuilder();
 
-        // int port = threadId==1 ? 11222 : 11322;
-        int port = 11222;
+        int port = threadId==1 ? 12232 : 13232;
+        //int port = 12232;
 
         return cacheConfigBuilder.persistence().addStore(RemoteStoreConfigurationBuilder.class)
                 .fetchPersistentState(false)
diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGSessionsCacheTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGSessionsCacheTest.java
index 056b0de..7101d38 100644
--- a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGSessionsCacheTest.java
+++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGSessionsCacheTest.java
@@ -39,6 +39,7 @@ import org.infinispan.persistence.manager.PersistenceManager;
 import org.infinispan.persistence.remote.RemoteStore;
 import org.infinispan.persistence.remote.configuration.ExhaustedAction;
 import org.jboss.logging.Logger;
+import org.junit.Assert;
 import org.keycloak.common.util.Time;
 import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
 import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
@@ -50,13 +51,9 @@ import org.keycloak.models.sessions.infinispan.remotestore.KcRemoteStoreConfigur
 import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
 
 /**
- * Test requires to prepare 2 JDG (or infinispan servers) before it's runned.
- * Steps:
- * - In JDG1/standalone/configuration/clustered.xml add this: <replicated-cache name="sessions" mode="SYNC" start="EAGER"/>
- * - Same in JDG2
- * - Run JDG1 with: ./standalone.sh -c clustered.xml
- * - Run JDG2 with: ./standalone.sh -c clustered.xml -Djboss.socket.binding.port-offset=100
- * - Run this test
+ * Test concurrency for remoteStore (backed by HotRod RemoteCaches) against external JDG. Especially tests "replaceWithVersion" contract.
+ *
+ * Steps: {@see ConcurrencyJDGRemoteCacheClientListenersTest}
  *
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
@@ -66,6 +63,9 @@ public class ConcurrencyJDGSessionsCacheTest {
 
     private static final int ITERATION_PER_WORKER = 1000;
 
+    private static RemoteCache remoteCache1;
+    private static RemoteCache remoteCache2;
+
     private static final AtomicInteger failedReplaceCounter = new AtomicInteger(0);
     private static final AtomicInteger failedReplaceCounter2 = new AtomicInteger(0);
 
@@ -176,6 +176,16 @@ public class ConcurrencyJDGSessionsCacheTest {
                 ", successfulListenerWrites: " + successfulListenerWrites.get() + ", successfulListenerWrites2: " + successfulListenerWrites2.get() +
                 ", failedReplaceCounter: " + failedReplaceCounter.get() + ", failedReplaceCounter2: " + failedReplaceCounter2.get() );
 
+        System.out.println("Sleeping before other report");
+
+        Thread.sleep(1000);
+
+        System.out.println("Finished. Took: " + took + " ms. Notes: " + cache1.get("123").getEntity().getNotes().size() +
+                ", successfulListenerWrites: " + successfulListenerWrites.get() + ", successfulListenerWrites2: " + successfulListenerWrites2.get() +
+                ", failedReplaceCounter: " + failedReplaceCounter.get() + ", failedReplaceCounter2: " + failedReplaceCounter2.get());
+
+
+
         // Finish JVM
         cache1.getCacheManager().stop();
         cache2.getCacheManager().stop();
@@ -186,7 +196,11 @@ public class ConcurrencyJDGSessionsCacheTest {
 
         RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache);
 
-        remoteCache.keySet();
+        if (threadId == 1) {
+            remoteCache1 = remoteCache;
+        } else {
+            remoteCache2 = remoteCache;
+        }
 
         AtomicInteger counter = threadId ==1 ? successfulListenerWrites : successfulListenerWrites2;
         HotRodListener listener = new HotRodListener(cache, remoteCache, counter);
@@ -224,8 +238,8 @@ public class ConcurrencyJDGSessionsCacheTest {
     private static Configuration getCacheBackedByRemoteStore(int threadId) {
         ConfigurationBuilder cacheConfigBuilder = new ConfigurationBuilder();
 
-        //int port = threadId==1 ? 11222 : 11322;
-        int port = 11222;
+        int port = threadId==1 ? 12232 : 13232;
+        //int port = 12232;
 
         return cacheConfigBuilder.persistence().addStore(KcRemoteStoreConfigurationBuilder.class)
                 .fetchPersistentState(false)
@@ -288,12 +302,12 @@ public class ConcurrencyJDGSessionsCacheTest {
 
     private static class RemoteCacheWorker extends Thread {
 
-        private final RemoteCache<String, UserSessionEntity> cache;
+        private final RemoteCache<String, UserSessionEntity> remoteCache;
 
         private final int myThreadId;
 
-        private RemoteCacheWorker(RemoteCache cache, int myThreadId) {
-            this.cache = cache;
+        private RemoteCacheWorker(RemoteCache remoteCache, int myThreadId) {
+            this.remoteCache = remoteCache;
             this.myThreadId = myThreadId;
         }
 
@@ -306,7 +320,7 @@ public class ConcurrencyJDGSessionsCacheTest {
 
                 boolean replaced = false;
                 while (!replaced) {
-                    VersionedValue<UserSessionEntity> versioned = cache.getVersioned("123");
+                    VersionedValue<UserSessionEntity> versioned = remoteCache.getVersioned("123");
                     UserSessionEntity oldSession = versioned.getValue();
                     //UserSessionEntity clone = DistributedCacheConcurrentWritesTest.cloneSession(oldSession);
                     UserSessionEntity clone = oldSession;
@@ -315,13 +329,20 @@ public class ConcurrencyJDGSessionsCacheTest {
                     //cache.replace("123", clone);
                     replaced = cacheReplace(versioned, clone);
                 }
+
+                // Try to see if remoteCache on 2nd DC is immediatelly seeing our change
+                RemoteCache secondDCRemoteCache = myThreadId == 1 ? remoteCache2 : remoteCache1;
+                UserSessionEntity thatSession = (UserSessionEntity) secondDCRemoteCache.get("123");
+
+                Assert.assertEquals("someVal", thatSession.getNotes().get(noteKey));
+                //System.out.println("Passed");
             }
 
         }
 
         private boolean cacheReplace(VersionedValue<UserSessionEntity> oldSession, UserSessionEntity newSession) {
             try {
-                boolean replaced = cache.replaceWithVersion("123", newSession, oldSession.getVersion());
+                boolean replaced = remoteCache.replaceWithVersion("123", newSession, oldSession.getVersion());
                 //cache.replace("123", newSession);
                 if (!replaced) {
                     failedReplaceCounter.incrementAndGet();
diff --git a/server-spi-private/src/main/java/org/keycloak/models/Constants.java b/server-spi-private/src/main/java/org/keycloak/models/Constants.java
index 40f9081..8ff0966 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/Constants.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/Constants.java
@@ -57,6 +57,8 @@ public interface Constants {
     String KEY = "key";
 
     String SKIP_LINK = "skipLink";
+    String TEMPLATE_ATTR_ACTION_URI = "actionUri";
+    String TEMPLATE_ATTR_REQUIRED_ACTIONS = "requiredActions";
 
     // Prefix for user attributes used in various "context"data maps
     String USER_ATTRIBUTES_PREFIX = "user.attributes.";
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java
index 9993ab7..a1c857f 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java
@@ -22,14 +22,20 @@ import org.keycloak.authentication.RequiredActionProvider;
 import org.keycloak.authentication.actiontoken.*;
 import org.keycloak.events.Errors;
 import org.keycloak.events.EventType;
+import org.keycloak.forms.login.LoginFormsProvider;
 import org.keycloak.models.*;
+import org.keycloak.models.UserModel.RequiredAction;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.protocol.oidc.utils.RedirectUtils;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.services.Urls;
 import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.messages.Messages;
 import org.keycloak.sessions.AuthenticationSessionModel;
 import java.util.Objects;
 import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
 
 /**
  *
@@ -64,6 +70,21 @@ public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHander<
     @Override
     public Response handleToken(ExecuteActionsActionToken token, ActionTokenContext<ExecuteActionsActionToken> tokenContext) {
         AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
+        final UriInfo uriInfo = tokenContext.getUriInfo();
+        final RealmModel realm = tokenContext.getRealm();
+        final KeycloakSession session = tokenContext.getSession();
+        if (tokenContext.isAuthenticationSessionFresh()) {
+            // Update the authentication session in the token
+            token.setAuthenticationSessionId(authSession.getId());
+            UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
+            String confirmUri = builder.build(realm.getName()).toString();
+
+            return session.getProvider(LoginFormsProvider.class)
+                    .setSuccess(Messages.CONFIRM_EXECUTION_OF_ACTIONS)
+                    .setAttribute(Constants.TEMPLATE_ATTR_ACTION_URI, confirmUri)
+                    .setAttribute(Constants.TEMPLATE_ATTR_REQUIRED_ACTIONS, token.getRequiredActions())
+                    .createInfoPage();
+        }
 
         String redirectUri = RedirectUtils.verifyRedirectUri(tokenContext.getUriInfo(), token.getRedirectUri(),
           tokenContext.getRealm(), authSession.getClient());
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java
index 7776634..39c6f9a 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java
@@ -30,6 +30,7 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
 
     private static final String JSON_FIELD_IDENTITY_PROVIDER_USERNAME = "idpu";
     private static final String JSON_FIELD_IDENTITY_PROVIDER_ALIAS = "idpa";
+    private static final String JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID = "oasid";
 
     @JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_USERNAME)
     private String identityProviderUsername;
@@ -37,6 +38,9 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
     @JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_ALIAS)
     private String identityProviderAlias;
 
+    @JsonProperty(value = JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID)
+    private String originalAuthenticationSessionId;
+
     public IdpVerifyAccountLinkActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId,
       String identityProviderUsername, String identityProviderAlias) {
         super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
@@ -62,4 +66,12 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
     public void setIdentityProviderAlias(String identityProviderAlias) {
         this.identityProviderAlias = identityProviderAlias;
     }
+
+    public String getOriginalAuthenticationSessionId() {
+        return originalAuthenticationSessionId;
+    }
+
+    public void setOriginalAuthenticationSessionId(String originalAuthenticationSessionId) {
+        this.originalAuthenticationSessionId = originalAuthenticationSessionId;
+    }
 }
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java
index bd56eea..c5dc897 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java
@@ -24,13 +24,18 @@ import org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAut
 import org.keycloak.events.*;
 import org.keycloak.forms.login.LoginFormsProvider;
 import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.services.Urls;
 import org.keycloak.services.managers.AuthenticationSessionManager;
 import org.keycloak.services.messages.Messages;
 import org.keycloak.sessions.AuthenticationSessionModel;
 import org.keycloak.sessions.AuthenticationSessionProvider;
 import java.util.Collections;
 import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
 
 /**
  * Action token handler for verification of e-mail address.
@@ -58,6 +63,9 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
     public Response handleToken(IdpVerifyAccountLinkActionToken token, ActionTokenContext<IdpVerifyAccountLinkActionToken> tokenContext) {
         UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser();
         EventBuilder event = tokenContext.getEvent();
+        final UriInfo uriInfo = tokenContext.getUriInfo();
+        final RealmModel realm = tokenContext.getRealm();
+        final KeycloakSession session = tokenContext.getSession();
 
         event.event(EventType.IDENTITY_PROVIDER_LINK_ACCOUNT)
           .detail(Details.EMAIL, user.getEmail())
@@ -65,16 +73,28 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
           .detail(Details.IDENTITY_PROVIDER_USERNAME, token.getIdentityProviderUsername())
           .success();
 
+        AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
+        if (tokenContext.isAuthenticationSessionFresh()) {
+            token.setOriginalAuthenticationSessionId(token.getAuthenticationSessionId());
+            token.setAuthenticationSessionId(authSession.getId());
+            UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
+            String confirmUri = builder.build(realm.getName()).toString();
+
+            return session.getProvider(LoginFormsProvider.class)
+                    .setSuccess(Messages.CONFIRM_ACCOUNT_LINKING, token.getIdentityProviderUsername(), token.getIdentityProviderAlias())
+                    .setAttribute(Constants.TEMPLATE_ATTR_ACTION_URI, confirmUri)
+                    .createInfoPage();
+        }
+
         // verify user email as we know it is valid as this entry point would never have gotten here.
         user.setEmailVerified(true);
 
-        AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
-        if (tokenContext.isAuthenticationSessionFresh()) {
-            AuthenticationSessionManager asm = new AuthenticationSessionManager(tokenContext.getSession());
-            asm.removeAuthenticationSession(tokenContext.getRealm(), authSession, true);
+        if (token.getOriginalAuthenticationSessionId() != null) {
+            AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
+            asm.removeAuthenticationSession(realm, authSession, true);
 
-            AuthenticationSessionProvider authSessProvider = tokenContext.getSession().authenticationSessions();
-            authSession = authSessProvider.getAuthenticationSession(tokenContext.getRealm(), token.getAuthenticationSessionId());
+            AuthenticationSessionProvider authSessProvider = session.authenticationSessions();
+            authSession = authSessProvider.getAuthenticationSession(realm, token.getOriginalAuthenticationSessionId());
 
             if (authSession != null) {
                 authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername());
@@ -85,7 +105,7 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
                 );
             }
 
-            return tokenContext.getSession().getProvider(LoginFormsProvider.class)
+            return session.getProvider(LoginFormsProvider.class)
                     .setSuccess(Messages.IDENTITY_PROVIDER_LINK_SUCCESS, token.getIdentityProviderAlias(), token.getIdentityProviderUsername())
                     .setAttribute(Constants.SKIP_LINK, true)
                     .createInfoPage();
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java
index 656c518..f9ebc6d 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java
@@ -29,10 +29,14 @@ public class VerifyEmailActionToken extends DefaultActionToken {
     public static final String TOKEN_TYPE = "verify-email";
 
     private static final String JSON_FIELD_EMAIL = "eml";
+    private static final String JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID = "oasid";
 
     @JsonProperty(value = JSON_FIELD_EMAIL)
     private String email;
 
+    @JsonProperty(value = JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID)
+    private String originalAuthenticationSessionId;
+
     public VerifyEmailActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId, String email) {
         super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
         this.email = email;
@@ -48,4 +52,12 @@ public class VerifyEmailActionToken extends DefaultActionToken {
     public void setEmail(String email) {
         this.email = email;
     }
+
+    public String getOriginalAuthenticationSessionId() {
+        return originalAuthenticationSessionId;
+    }
+
+    public void setOriginalAuthenticationSessionId(String originalAuthenticationSessionId) {
+        this.originalAuthenticationSessionId = originalAuthenticationSessionId;
+    }
 }
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java
index abe2127..b5d046e 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java
@@ -21,14 +21,20 @@ import org.keycloak.TokenVerifier.Predicate;
 import org.keycloak.authentication.actiontoken.*;
 import org.keycloak.events.*;
 import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserModel.RequiredAction;
+import org.keycloak.services.Urls;
 import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.managers.AuthenticationSessionManager;
 import org.keycloak.services.messages.Messages;
 import org.keycloak.sessions.AuthenticationSessionModel;
 import java.util.Objects;
 import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
 
 /**
  * Action token handler for verification of e-mail address.
@@ -57,13 +63,29 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHander<Ver
     }
 
     @Override
-        public Response handleToken(VerifyEmailActionToken token, ActionTokenContext<VerifyEmailActionToken> tokenContext) {
+    public Response handleToken(VerifyEmailActionToken token, ActionTokenContext<VerifyEmailActionToken> tokenContext) {
         UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser();
         EventBuilder event = tokenContext.getEvent();
 
         event.event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail());
 
         AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
+        final UriInfo uriInfo = tokenContext.getUriInfo();
+        final RealmModel realm = tokenContext.getRealm();
+        final KeycloakSession session = tokenContext.getSession();
+
+        if (tokenContext.isAuthenticationSessionFresh()) {
+            // Update the authentication session in the token
+            token.setOriginalAuthenticationSessionId(token.getAuthenticationSessionId());
+            token.setAuthenticationSessionId(authSession.getId());
+            UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
+            String confirmUri = builder.build(realm.getName()).toString();
+
+            return session.getProvider(LoginFormsProvider.class)
+                    .setSuccess(Messages.CONFIRM_EMAIL_ADDRESS_VERIFICATION, user.getEmail())
+                    .setAttribute(Constants.TEMPLATE_ATTR_ACTION_URI, confirmUri)
+                    .createInfoPage();
+        }
 
         // verify user email as we know it is valid as this entry point would never have gotten here.
         user.setEmailVerified(true);
@@ -72,9 +94,10 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHander<Ver
 
         event.success();
 
-        if (tokenContext.isAuthenticationSessionFresh()) {
+        if (token.getOriginalAuthenticationSessionId() != null) {
             AuthenticationSessionManager asm = new AuthenticationSessionManager(tokenContext.getSession());
             asm.removeAuthenticationSession(tokenContext.getRealm(), authSession, true);
+
             return tokenContext.getSession().getProvider(LoginFormsProvider.class)
                     .setSuccess(Messages.EMAIL_VERIFIED)
                     .createInfoPage();
@@ -82,8 +105,8 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHander<Ver
 
         tokenContext.setEvent(event.clone().removeDetail(Details.EMAIL).event(EventType.LOGIN));
 
-        String nextAction = AuthenticationManager.nextRequiredAction(tokenContext.getSession(), authSession, tokenContext.getClientConnection(), tokenContext.getRequest(), tokenContext.getUriInfo(), event);
-        return AuthenticationManager.redirectToRequiredActions(tokenContext.getSession(), tokenContext.getRealm(), authSession, tokenContext.getUriInfo(), nextAction);
+        String nextAction = AuthenticationManager.nextRequiredAction(session, authSession, tokenContext.getClientConnection(), tokenContext.getRequest(), uriInfo, event);
+        return AuthenticationManager.redirectToRequiredActions(session, realm, authSession, uriInfo, nextAction);
     }
 
 }
diff --git a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java
index abc23a1..ddf29a1 100755
--- a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java
+++ b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java
@@ -97,7 +97,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
 
     @Override
     public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException {
-        Map<String, Object> attributes = new HashMap<String, Object>();
+        Map<String, Object> attributes = new HashMap<String, Object>(this.attributes);
         attributes.put("user", new ProfileBean(user));
         attributes.put("link", link);
         attributes.put("linkExpiration", expirationInMinutes);
@@ -112,7 +112,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
         setRealm(session.getContext().getRealm());
         setUser(user);
 
-        Map<String, Object> attributes = new HashMap<String, Object>();
+        Map<String, Object> attributes = new HashMap<String, Object>(this.attributes);
         attributes.put("user", new ProfileBean(user));
         attributes.put("realmName", realm.getName());
 
@@ -122,7 +122,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
 
     @Override
     public void sendConfirmIdentityBrokerLink(String link, long expirationInMinutes) throws EmailException {
-        Map<String, Object> attributes = new HashMap<String, Object>();
+        Map<String, Object> attributes = new HashMap<String, Object>(this.attributes);
         attributes.put("user", new ProfileBean(user));
         attributes.put("link", link);
         attributes.put("linkExpiration", expirationInMinutes);
@@ -142,7 +142,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
 
     @Override
     public void sendExecuteActions(String link, long expirationInMinutes) throws EmailException {
-        Map<String, Object> attributes = new HashMap<String, Object>();
+        Map<String, Object> attributes = new HashMap<String, Object>(this.attributes);
         attributes.put("user", new ProfileBean(user));
         attributes.put("link", link);
         attributes.put("linkExpiration", expirationInMinutes);
@@ -155,7 +155,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
 
     @Override
     public void sendVerifyEmail(String link, long expirationInMinutes) throws EmailException {
-        Map<String, Object> attributes = new HashMap<String, Object>();
+        Map<String, Object> attributes = new HashMap<String, Object>(this.attributes);
         attributes.put("user", new ProfileBean(user));
         attributes.put("link", link);
         attributes.put("linkExpiration", expirationInMinutes);
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
index d7eb01c..8ec6a5b 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -449,7 +449,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
     public Response createIdpLinkEmailPage() {
         BrokeredIdentityContext brokerContext = (BrokeredIdentityContext) this.attributes.get(IDENTITY_PROVIDER_BROKER_CONTEXT);
         String idpAlias = brokerContext.getIdpConfig().getAlias();
-        idpAlias = ObjectUtil.capitalize(idpAlias);;
+        idpAlias = ObjectUtil.capitalize(idpAlias);
         setMessage(MessageType.WARNING, Messages.LINK_IDP, idpAlias);
 
         return createResponse(LoginFormsPages.LOGIN_IDP_LINK_EMAIL);
diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java
index 710779e..180694a 100755
--- a/services/src/main/java/org/keycloak/services/messages/Messages.java
+++ b/services/src/main/java/org/keycloak/services/messages/Messages.java
@@ -160,6 +160,12 @@ public class Messages {
 
     public static final String IDENTITY_PROVIDER_LINK_SUCCESS = "identityProviderLinkSuccess";
 
+    public static final String CONFIRM_ACCOUNT_LINKING = "confirmAccountLinking";
+
+    public static final String CONFIRM_EMAIL_ADDRESS_VERIFICATION = "confirmEmailAddressVerification";
+
+    public static final String CONFIRM_EXECUTION_OF_ACTIONS = "confirmExecutionOfActions";
+
     public static final String STALE_CODE = "staleCodeMessage";
 
     public static final String STALE_CODE_ACCOUNT = "staleCodeAccountMessage";
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
index bf3b236..fbd318a 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
@@ -704,6 +704,7 @@ public class UserResource {
             String link = builder.build(realm.getName()).toString();
 
             this.session.getProvider(EmailTemplateProvider.class)
+              .setAttribute(Constants.TEMPLATE_ATTR_REQUIRED_ACTIONS, token.getRequiredActions())
               .setRealm(realm)
               .setUser(user)
               .sendExecuteActions(link, TimeUnit.SECONDS.toMinutes(lifespan));
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java
index ca5cb76..b64cf03 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java
@@ -37,6 +37,7 @@ import org.keycloak.testsuite.pages.InfoPage;
 import org.keycloak.testsuite.pages.LoginExpiredPage;
 import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
 import org.keycloak.testsuite.pages.LoginUpdateProfileEditUsernameAllowedPage;
+import org.keycloak.testsuite.pages.ProceedPage;
 import org.keycloak.testsuite.rule.KeycloakRule;
 import org.keycloak.testsuite.rule.WebResource;
 import org.keycloak.testsuite.rule.WebRule;
@@ -52,6 +53,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import org.hamcrest.Matchers;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.startsWith;
 import static org.junit.Assert.assertEquals;
@@ -345,6 +347,9 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi
         // Go to the same link again
         driver.navigate().to(linkFromMail.trim());
 
+        proceedPage.assertCurrent();
+        Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Confirm linking the account"));
+        proceedPage.clickProceedLink();
         infoPage.assertCurrent();
         Assert.assertThat(infoPage.getInfo(), startsWith("You successfully verified your email. Please go back to your original browser and continue there with the login."));
     }
@@ -379,10 +384,14 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi
 
             WebDriver driver2 = webRule2.getDriver();
             InfoPage infoPage2 = webRule2.getPage(InfoPage.class);
+            ProceedPage proceedPage2 = webRule2.getPage(ProceedPage.class);
 
             driver2.navigate().to(linkFromMail.trim());
 
             // authenticated, but not redirected to app. Just seeing info page.
+            proceedPage2.assertCurrent();
+            Assert.assertThat(proceedPage2.getInfo(), Matchers.containsString("Confirm linking the account"));
+            proceedPage2.clickProceedLink();
             infoPage2.assertCurrent();
             Assert.assertThat(infoPage2.getInfo(), startsWith("You successfully verified your email. Please go back to your original browser and continue there with the login."));
         } finally {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
index 297d00a..c854e1e 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
@@ -110,6 +110,9 @@ public abstract class AbstractIdentityProviderTest {
     @WebResource
     protected InfoPage infoPage;
 
+    @WebResource
+    protected ProceedPage proceedPage;
+
     protected KeycloakSession session;
 
     protected int logoutTimeOffset = 0;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/ProceedPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/ProceedPage.java
new file mode 100644
index 0000000..97d7c28
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/ProceedPage.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.pages;
+
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class ProceedPage extends AbstractPage {
+
+    @FindBy(className = "instruction")
+    private WebElement infoMessage;
+
+    @FindBy(linkText = "» Click here to proceed")
+    private WebElement proceedLink;
+
+    public String getInfo() {
+        return infoMessage.getText();
+    }
+
+    public boolean isCurrent() {
+        return driver.getPageSource().contains("kc-info-message") && proceedLink.isDisplayed();
+    }
+
+    @Override
+    public void open() {
+        throw new UnsupportedOperationException();
+    }
+
+    public void clickProceedLink() {
+        proceedLink.click();
+    }
+
+}
diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl
index bdd8b7c..b6fbd2e 100644
--- a/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl
+++ b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl
@@ -47,7 +47,7 @@
         <xsl:copy>
             <xsl:apply-templates select="@* | node()" />
 
-            <replicated-cache-configuration name="sessions-cfg" mode="SYNC" start="EAGER" batching="false">
+            <replicated-cache-configuration name="sessions-cfg" mode="ASYNC" start="EAGER" batching="false">
                 <transaction mode="NON_XA" locking="PESSIMISTIC"/>
             </replicated-cache-configuration>
 
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/ProceedPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/ProceedPage.java
new file mode 100644
index 0000000..97d7c28
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/ProceedPage.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.pages;
+
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class ProceedPage extends AbstractPage {
+
+    @FindBy(className = "instruction")
+    private WebElement infoMessage;
+
+    @FindBy(linkText = "» Click here to proceed")
+    private WebElement proceedLink;
+
+    public String getInfo() {
+        return infoMessage.getText();
+    }
+
+    public boolean isCurrent() {
+        return driver.getPageSource().contains("kc-info-message") && proceedLink.isDisplayed();
+    }
+
+    @Override
+    public void open() {
+        throw new UnsupportedOperationException();
+    }
+
+    public void clickProceedLink() {
+        proceedLink.click();
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
index e366ef5..f4c7452 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
@@ -37,6 +37,7 @@ import org.keycloak.testsuite.admin.ApiUtil;
 import org.keycloak.testsuite.auth.page.AuthRealm;
 import org.keycloak.testsuite.pages.AppPage;
 import org.keycloak.testsuite.pages.AppPage.RequestType;
+import org.keycloak.testsuite.pages.ProceedPage;
 import org.keycloak.testsuite.pages.ErrorPage;
 import org.keycloak.testsuite.pages.InfoPage;
 import org.keycloak.testsuite.pages.LoginPage;
@@ -82,6 +83,9 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
     protected InfoPage infoPage;
 
     @Page
+    protected ProceedPage proceedPage;
+
+    @Page
     protected ErrorPage errorPage;
 
     private String testUserId;
@@ -330,6 +334,8 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
 
         driver.navigate().to(verificationUrl2.trim());
 
+        proceedPage.assertCurrent();
+        proceedPage.clickProceedLink();
         infoPage.assertCurrent();
         assertEquals("Your email address has been verified.", infoPage.getInfo());
     }
@@ -355,6 +361,9 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
         driver.manage().deleteAllCookies();
 
         driver.navigate().to(verificationUrl.trim());
+        proceedPage.assertCurrent();
+        proceedPage.clickProceedLink();
+        infoPage.assertCurrent();
 
         events.expectRequiredAction(EventType.VERIFY_EMAIL)
           .user(testUserId)
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java
index c953a9c..5559a5e 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java
@@ -19,16 +19,17 @@ package org.keycloak.testsuite.admin.concurrency;
 
 import org.keycloak.admin.client.Keycloak;
 import org.keycloak.admin.client.resource.RealmResource;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
-import org.keycloak.testsuite.admin.AbstractAdminTest;
+import java.util.LinkedList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 
 
 /**
@@ -36,77 +37,72 @@ import org.keycloak.testsuite.admin.AbstractAdminTest;
  */
 public abstract class AbstractConcurrencyTest extends AbstractTestRealmKeycloakTest {
 
-    private static final int DEFAULT_THREADS = 5;
-    private static final int DEFAULT_ITERATIONS = 20;
+    private static final int DEFAULT_THREADS = 4;
+    private static final int DEFAULT_NUMBER_OF_EXECUTIONS = 20 * DEFAULT_THREADS;
 
     public static final String REALM_NAME = "test";
 
     // If enabled only one request is allowed at the time. Useful for checking that test is working.
     private static final boolean SYNCHRONIZED = false;
 
-    protected void run(final KeycloakRunnable runnable) throws Throwable {
-        run(runnable, DEFAULT_THREADS, DEFAULT_ITERATIONS);
-    }
-
     @Override
     public void configureTestRealm(RealmRepresentation testRealm) {
     }
 
-    protected void run(final KeycloakRunnable runnable, final int numThreads, final int numIterationsPerThread) throws Throwable {
-        final CountDownLatch latch = new CountDownLatch(numThreads);
-        final AtomicReference<Throwable> failed = new AtomicReference();
-        final List<Thread> threads = new LinkedList<>();
-        final Lock lock = SYNCHRONIZED ? new ReentrantLock() : null;
-
-        for (int t = 0; t < numThreads; t++) {
-            final int threadNum = t;
-            Thread thread = new Thread() {
-                @Override
-                public void run() {
-                    Keycloak keycloak = null;
-                    try {
-                        if (lock != null) {
-                            lock.lock();
-                        }
-
-                        keycloak = Keycloak.getInstance(getAuthServerRoot().toString(), "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
-                        RealmResource realm = keycloak.realm(REALM_NAME);
-                        for (int i = 0; i < numIterationsPerThread && latch.getCount() > 0; i++) {
-                            log.infov("thread {0}, iteration {1}", threadNum, i);
-                            runnable.run(keycloak, realm, threadNum, i);
-                        }
-                        latch.countDown();
-                    } catch (Throwable t) {
-                        failed.compareAndSet(null, t);
-                        while (latch.getCount() > 0) {
-                            latch.countDown();
-                        }
-                    } finally {
-                        keycloak.close();
-                        if (lock != null) {
-                            lock.unlock();
-                        }
-                    }
+    protected void run(final KeycloakRunnable... runnables) {
+        run(DEFAULT_THREADS, DEFAULT_NUMBER_OF_EXECUTIONS, runnables);
+    }
+
+    protected void run(final int numThreads, final int totalNumberOfExecutions, final KeycloakRunnable... runnables) {
+        final ExecutorService service = SYNCHRONIZED
+          ? Executors.newSingleThreadExecutor()
+          : Executors.newFixedThreadPool(numThreads);
+
+        ThreadLocal<Keycloak> keycloaks = new ThreadLocal<Keycloak>() {
+            @Override
+            protected Keycloak initialValue() {
+                return Keycloak.getInstance(getAuthServerRoot().toString(), "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
+            }
+        };
+
+        AtomicInteger currentThreadIndex = new AtomicInteger();
+        Collection<Callable<Void>> tasks = new LinkedList<>();
+        Collection<Throwable> failures = new ConcurrentLinkedQueue<>();
+        final List<Callable<Void>> runnablesToTasks = new LinkedList<>();
+        for (KeycloakRunnable runnable : runnables) {
+            runnablesToTasks.add(() -> {
+                int arrayIndex = currentThreadIndex.getAndIncrement() % numThreads;
+                try {
+                    runnable.run(arrayIndex % numThreads, keycloaks.get(), keycloaks.get().realm(REALM_NAME));
+                } catch (Throwable ex) {
+                    failures.add(ex);
+                    log.error(ex.getMessage(), ex);
                 }
-            };
-            thread.start();
-            threads.add(thread);
+                return null;
+            });
+        }
+        for (int i = 0; i < totalNumberOfExecutions; i ++) {
+            runnablesToTasks.forEach(tasks::add);
         }
 
-        latch.await();
-
-        for (Thread t : threads) {
-            t.join();
+        try {
+            service.invokeAll(tasks);
+            service.shutdown();
+            service.awaitTermination(3, TimeUnit.MINUTES);
+        } catch (InterruptedException ex) {
+            throw new RuntimeException(ex);
         }
 
-        if (failed.get() != null) {
-            throw failed.get();
+        if (! failures.isEmpty()) {
+            RuntimeException ex = new RuntimeException("There were failures in threads. Failures count: " + failures.size());
+            failures.forEach(ex::addSuppressed);
+            throw ex;
         }
     }
 
     protected interface KeycloakRunnable {
 
-        void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum);
+        void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable;
 
     }
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrencyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrencyTest.java
index f3c66b5..2d2053b 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrencyTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrencyTest.java
@@ -17,12 +17,12 @@
 
 package org.keycloak.testsuite.admin.concurrency;
 
-import org.junit.Assert;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.keycloak.admin.client.Keycloak;
 import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.ClientsResource;
 import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.RolesResource;
 import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.representations.idm.GroupRepresentation;
 import org.keycloak.representations.idm.RoleRepresentation;
@@ -31,7 +31,11 @@ import javax.ws.rs.NotFoundException;
 import javax.ws.rs.core.Response;
 import org.keycloak.testsuite.admin.ApiUtil;
 
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 /**
@@ -39,203 +43,198 @@ import static org.junit.Assert.fail;
  */
 public class ConcurrencyTest extends AbstractConcurrencyTest {
 
-    boolean passedCreateClient = false;
-    boolean passedCreateRole = false;
+    public void concurrentTest(KeycloakRunnable... tasks) throws Throwable {
+        System.out.println("***************************");
+        long start = System.currentTimeMillis();
+        run(tasks);
+        long end = System.currentTimeMillis() - start;
+        System.out.println("took " + end + " ms");
+    }
 
-    //@Test
+    @Test
     public void testAllConcurrently() throws Throwable {
-        Thread client = new Thread(new Runnable() {
-            @Override
-            public void run() {
-                try {
-                    createClient();
-                    passedCreateClient = true;
-                } catch (Throwable throwable) {
-                    throw new RuntimeException(throwable);
-                }
-            }
-        });
-        Thread role = new Thread(new Runnable() {
-            @Override
-            public void run() {
-                try {
-                    createRole();
-                    passedCreateRole = true;
-                } catch (Throwable throwable) {
-                    throw new RuntimeException(throwable);
-                }
-            }
-        });
-
-        client.start();
-        role.start();
-        client.join();
-        role.join();
-        Assert.assertTrue(passedCreateClient);
-        Assert.assertTrue(passedCreateRole);
+        AtomicInteger uniqueCounter = new AtomicInteger(100000);
+        concurrentTest(
+          new CreateClient(uniqueCounter),
+          new CreateRemoveClient(uniqueCounter),
+          new CreateGroup(uniqueCounter),
+          new CreateRole(uniqueCounter)
+        );
     }
 
     @Test
     public void createClient() throws Throwable {
-        System.out.println("***************************");
-        long start = System.currentTimeMillis();
-        run(new KeycloakRunnable() {
-            @Override
-            public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) {
-                String name = "c-" + threadNum + "-" + iterationNum;
-                ClientRepresentation c = new ClientRepresentation();
-                c.setClientId(name);
-                Response response = realm.clients().create(c);
-                String id = ApiUtil.getCreatedId(response);
-                response.close();
-
-                c = realm.clients().get(id).toRepresentation();
-                assertNotNull(c);
-                boolean found = false;
-                for (ClientRepresentation r : realm.clients().findAll()) {
-                    if (r.getClientId().equals(name)) {
-                        found = true;
-                        break;
-                    }
-                }
-                if (!found) {
-                    fail("Client " + name + " not found in client list");
-                }
-            }
-        });
-        long end = System.currentTimeMillis() - start;
-        System.out.println("createClient took " + end);
-
+        AtomicInteger uniqueCounter = new AtomicInteger();
+        concurrentTest(new CreateClient(uniqueCounter));
     }
 
     @Test
     public void createGroup() throws Throwable {
-        System.out.println("***************************");
-        long start = System.currentTimeMillis();
-        run(new KeycloakRunnable() {
-            @Override
-            public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) {
-                String name = "c-" + threadNum + "-" + iterationNum;
-                GroupRepresentation c = new GroupRepresentation();
-                c.setName(name);
-                Response response = realm.groups().add(c);
-                String id = ApiUtil.getCreatedId(response);
-                response.close();
-
-                c = realm.groups().group(id).toRepresentation();
-                assertNotNull(c);
-                boolean found = false;
-                for (GroupRepresentation r : realm.groups().groups()) {
-                    if (r.getName().equals(name)) {
-                        found = true;
-                        break;
-                    }
-                }
-                if (!found) {
-                    fail("Group " + name + " not found in group list");
-                }
-            }
-        });
-        long end = System.currentTimeMillis() - start;
-        System.out.println("createGroup took " + end);
-
+        AtomicInteger uniqueCounter = new AtomicInteger();
+        concurrentTest(new CreateGroup(uniqueCounter));
     }
 
     @Test
-    @Ignore
     public void createRemoveClient() throws Throwable {
         // FYI< this will fail as HSQL seems to be trying to perform table locks.
-        System.out.println("***************************");
-        long start = System.currentTimeMillis();
-        run(new KeycloakRunnable() {
-            @Override
-            public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) {
-                String name = "c-" + threadNum + "-" + iterationNum;
-                ClientRepresentation c = new ClientRepresentation();
-                c.setClientId(name);
-                Response response = realm.clients().create(c);
-                String id = ApiUtil.getCreatedId(response);
-                response.close();
-
-                c = realm.clients().get(id).toRepresentation();
-                assertNotNull(c);
-                boolean found = false;
-                for (ClientRepresentation r : realm.clients().findAll()) {
-                    if (r.getClientId().equals(name)) {
-                        found = true;
-                        break;
-                    }
-                }
-                if (!found) {
-                    fail("Client " + name + " not found in client list");
-                }
-                realm.clients().get(id).remove();
-                try {
-                    c = realm.clients().get(id).toRepresentation();
-                    fail("Client " + name + " should not be found.  Should throw a 404");
-                } catch (NotFoundException e) {
-
-                }
-                found = false;
-                for (ClientRepresentation r : realm.clients().findAll()) {
-                    if (r.getClientId().equals(name)) {
-                        found = true;
-                        break;
-                    }
-                }
-                Assert.assertFalse("Client " + name + " should not be in client list", found);
-
-            }
-        });
-        long end = System.currentTimeMillis() - start;
-        System.out.println("createClient took " + end);
-
-    }
-
-
-    @Test
-    public void createRole() throws Throwable {
-        long start = System.currentTimeMillis();
-        run(new KeycloakRunnable() {
-            @Override
-            public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) {
-                String name = "r-" + threadNum + "-" + iterationNum;
-                RoleRepresentation r = new RoleRepresentation(name, null, false);
-                realm.roles().create(r);
-                assertNotNull(realm.roles().get(name).toRepresentation());
-            }
-        });
-        long end = System.currentTimeMillis() - start;
-        System.out.println("createRole took " + end);
-
+        AtomicInteger uniqueCounter = new AtomicInteger();
+        concurrentTest(new CreateRemoveClient(uniqueCounter));
     }
 
     @Test
     public void createClientRole() throws Throwable {
-        long start = System.currentTimeMillis();
         ClientRepresentation c = new ClientRepresentation();
         c.setClientId("client");
         Response response = adminClient.realm(REALM_NAME).clients().create(c);
         final String clientId = ApiUtil.getCreatedId(response);
         response.close();
 
-        System.out.println("*********************************************");
+        AtomicInteger uniqueCounter = new AtomicInteger();
+        concurrentTest(new CreateClientRole(uniqueCounter, clientId));
+    }
+
+    @Test
+    public void createRole() throws Throwable {
+        AtomicInteger uniqueCounter = new AtomicInteger();
+        run(new CreateRole(uniqueCounter));
+    }
 
-        run(new KeycloakRunnable() {
-            @Override
-            public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) {
-                String name = "r-" + threadNum + "-" + iterationNum;
-                RoleRepresentation r = new RoleRepresentation(name, null, false);
+    private class CreateClient implements KeycloakRunnable {
+
+        private final AtomicInteger clientIndex;
+
+        public CreateClient(AtomicInteger clientIndex) {
+            this.clientIndex = clientIndex;
+        }
+
+        @Override
+        public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable {
+            String name = "c-" + clientIndex.getAndIncrement();
+            ClientRepresentation c = new ClientRepresentation();
+            c.setClientId(name);
+            Response response = realm.clients().create(c);
+            String id = ApiUtil.getCreatedId(response);
+            response.close();
+
+            c = realm.clients().get(id).toRepresentation();
+            assertNotNull(c);
+            assertTrue("Client " + name + " not found in client list",
+              realm.clients().findAll().stream()
+                .map(ClientRepresentation::getClientId)
+                .filter(Objects::nonNull)
+                .anyMatch(name::equals));
+        }
+    }
 
-                ClientResource client = realm.clients().get(clientId);
-                client.roles().create(r);
+    private class CreateRemoveClient implements KeycloakRunnable {
+
+        private final AtomicInteger clientIndex;
+
+        public CreateRemoveClient(AtomicInteger clientIndex) {
+            this.clientIndex = clientIndex;
+        }
+
+        @Override
+        public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable {
+            String name = "c-" + clientIndex.getAndIncrement();
+            ClientRepresentation c = new ClientRepresentation();
+            c.setClientId(name);
+            final ClientsResource clients = realm.clients();
+
+            Response response = clients.create(c);
+            String id = ApiUtil.getCreatedId(response);
+            response.close();
+            final ClientResource client = clients.get(id);
+
+            c = client.toRepresentation();
+            assertNotNull(c);
+            assertTrue("Client " + name + " not found in client list",
+              clients.findAll().stream()
+                .map(ClientRepresentation::getClientId)
+                .filter(Objects::nonNull)
+                .anyMatch(name::equals));
+
+            client.remove();
+            try {
+                client.toRepresentation();
+                fail("Client " + name + " should not be found.  Should throw a 404");
+            } catch (NotFoundException e) {
 
-                assertNotNull(client.roles().get(name).toRepresentation());
             }
-        });
-        long end = System.currentTimeMillis() - start;
-        System.out.println("createClientRole took " + end);
-        System.out.println("*********************************************");
 
+            assertFalse("Client " + name + " should now not present in client list",
+              clients.findAll().stream()
+                .map(ClientRepresentation::getClientId)
+                .filter(Objects::nonNull)
+                .anyMatch(name::equals));
+        }
     }
+
+    private class CreateGroup implements KeycloakRunnable {
+
+        private final AtomicInteger uniqueIndex;
+
+        public CreateGroup(AtomicInteger uniqueIndex) {
+            this.uniqueIndex = uniqueIndex;
+        }
+
+        @Override
+        public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable {
+            String name = "g-" + uniqueIndex.getAndIncrement();
+            GroupRepresentation c = new GroupRepresentation();
+            c.setName(name);
+            Response response = realm.groups().add(c);
+            String id = ApiUtil.getCreatedId(response);
+            response.close();
+
+            c = realm.groups().group(id).toRepresentation();
+            assertNotNull(c);
+            assertTrue("Group " + name + " not found in group list",
+              realm.groups().groups().stream()
+                .map(GroupRepresentation::getName)
+                .filter(Objects::nonNull)
+                .anyMatch(name::equals));
+        }
+    }
+
+    private class CreateClientRole implements KeycloakRunnable {
+
+        private final AtomicInteger uniqueCounter;
+        private final String clientId;
+
+        public CreateClientRole(AtomicInteger uniqueCounter, String clientId) {
+            this.uniqueCounter = uniqueCounter;
+            this.clientId = clientId;
+        }
+
+        @Override
+        public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable {
+            String name = "cr-" + uniqueCounter.getAndIncrement();
+            RoleRepresentation r = new RoleRepresentation(name, null, false);
+
+            final RolesResource roles = realm.clients().get(clientId).roles();
+            roles.create(r);
+            assertNotNull(roles.get(name).toRepresentation());
+        }
+    }
+
+    private class CreateRole implements KeycloakRunnable {
+
+        private final AtomicInteger uniqueCounter;
+
+        public CreateRole(AtomicInteger uniqueCounter) {
+            this.uniqueCounter = uniqueCounter;
+        }
+
+        @Override
+        public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable {
+            String name = "r-" + uniqueCounter.getAndIncrement();
+            RoleRepresentation r = new RoleRepresentation(name, null, false);
+
+            final RolesResource roles = realm.roles();
+            roles.create(r);
+            assertNotNull(roles.get(name).toRepresentation());
+        }
+    }
+
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java
index 1208dc9..11e3bc0 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java
@@ -17,9 +17,7 @@
 
 package org.keycloak.testsuite.admin.concurrency;
 
-import java.io.BufferedReader;
 import java.io.IOException;
-import java.io.InputStreamReader;
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -28,8 +26,6 @@ import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.atomic.AtomicReference;
-
 import javax.ws.rs.core.Response;
 import org.apache.http.NameValuePair;
 import org.apache.http.client.entity.UrlEncodedFormEntity;
@@ -50,11 +46,13 @@ import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.keycloak.OAuth2Constants;
-import org.keycloak.admin.client.Keycloak;
-import org.keycloak.admin.client.resource.RealmResource;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.testsuite.util.OAuthClient;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import org.hamcrest.Matchers;
+
 
 
 /**
@@ -63,7 +61,6 @@ import org.keycloak.testsuite.util.OAuthClient;
 public class ConcurrentLoginTest extends AbstractConcurrencyTest {
     
     private static final int DEFAULT_THREADS = 10;
-    private static final int DEFAULT_ITERATIONS = 20;
     private static final int CLIENTS_PER_THREAD = 10;
     private static final int DEFAULT_CLIENTS_COUNT = CLIENTS_PER_THREAD * DEFAULT_THREADS;
     
@@ -89,11 +86,6 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
         log.debug("clients created");
     }
 
-    @Override
-    protected void run(final KeycloakRunnable runnable) throws Throwable {
-        run(runnable, DEFAULT_THREADS, DEFAULT_ITERATIONS);
-    }
-
     @Test
     public void concurrentLogin() throws Throwable {
         System.out.println("*********************************************");
@@ -108,42 +100,39 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
             log.debug("Executing login request");
             
             Assert.assertTrue(parseAndCloseResponse(httpClient.execute(request)).contains("<title>AUTH_RESPONSE</title>"));
-
-            run(new KeycloakRunnable() {
+            AtomicInteger clientIndex = new AtomicInteger();
+            ThreadLocal<OAuthClient> oauthClient = new ThreadLocal<OAuthClient>() {
                 @Override
-                public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) {
-                    OAuthClient oauth = new OAuthClient();
-                    oauth.init(adminClient, driver);
-
-                    int startIndex = CLIENTS_PER_THREAD * threadNum;
-                    for (int i = startIndex; i < startIndex + CLIENTS_PER_THREAD; i++) {
-                        oauth.clientId("client" + i);
-                        log.trace("Accessing login page for " + oauth.getClientId() + " thread " + threadNum + " iteration " + iterationNum);
-                        try {
-                            final HttpClientContext context = HttpClientContext.create();
-
-                            String pageContent = getPageContent(oauth.getLoginFormUrl(), httpClient, context);
-                            String currentUrl = context.getRedirectLocations().get(0).toString();
-
-                            Assert.assertTrue(pageContent.contains("<title>AUTH_RESPONSE</title>"));
-
-                            String code = getQueryFromUrl(currentUrl).get(OAuth2Constants.CODE);
-                            OAuthClient.AccessTokenResponse accessRes = oauth.doAccessTokenRequest(code, "password");
-                            Assert.assertEquals("AccessTokenResponse: error: '" + accessRes.getError() + "' desc: '" + accessRes.getErrorDescription() + "'",
-                                    200, accessRes.getStatusCode());
-
-                            OAuthClient.AccessTokenResponse refreshRes = oauth.doRefreshTokenRequest(accessRes.getRefreshToken(), "password");
-                            Assert.assertEquals("AccessTokenResponse: error: '" + refreshRes.getError() + "' desc: '" + refreshRes.getErrorDescription() + "'",
-                                    200, refreshRes.getStatusCode());
-
-                            if (userSessionId.get() == null) {
-                                AccessToken token = oauth.verifyToken(accessRes.getAccessToken());
-                                userSessionId.set(token.getSessionState());
-                            }
-                        } catch (Exception ex) {
-                            throw new RuntimeException(ex);
-                        }
-                    }
+                protected OAuthClient initialValue() {
+                    OAuthClient oauth1 = new OAuthClient();
+                    oauth1.init(adminClient, driver);
+                    return oauth1;
+                }
+            };
+
+            run(DEFAULT_THREADS, DEFAULT_CLIENTS_COUNT, (threadIndex, keycloak, realm) -> {
+                int i = clientIndex.getAndIncrement();
+                OAuthClient oauth1 = oauthClient.get();
+                oauth1.clientId("client" + i);
+                log.infof("%d [%s]: Accessing login page for %s", threadIndex, Thread.currentThread().getName(), oauth1.getClientId());
+
+                final HttpClientContext context = HttpClientContext.create();
+                String pageContent = getPageContent(oauth1.getLoginFormUrl(), httpClient, context);
+                String currentUrl = context.getRedirectLocations().get(0).toString();
+                Assert.assertThat(pageContent, Matchers.containsString("<title>AUTH_RESPONSE</title>"));
+                String code = getQueryFromUrl(currentUrl).get(OAuth2Constants.CODE);
+
+                OAuthClient.AccessTokenResponse accessRes = oauth1.doAccessTokenRequest(code, "password");
+                Assert.assertEquals("AccessTokenResponse: error: '" + accessRes.getError() + "' desc: '" + accessRes.getErrorDescription() + "'",
+                  200, accessRes.getStatusCode());
+
+                OAuthClient.AccessTokenResponse refreshRes = oauth1.doRefreshTokenRequest(accessRes.getRefreshToken(), "password");
+                Assert.assertEquals("AccessTokenResponse: error: '" + refreshRes.getError() + "' desc: '" + refreshRes.getErrorDescription() + "'",
+                  200, refreshRes.getStatusCode());
+
+                if (userSessionId.get() == null) {
+                    AccessToken token = oauth.verifyToken(accessRes.getAccessToken());
+                    userSessionId.set(token.getSessionState());
                 }
             });
 
@@ -154,15 +143,13 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
         }
     }
 
-
     protected void logStats(long start) {
         long end = System.currentTimeMillis() - start;
         log.info("concurrentLogin took " + (end/1000) + "s");
         log.info("*********************************************");
     }
-
     
-    private String getPageContent(String url, CloseableHttpClient httpClient, HttpClientContext context) throws Exception {
+    private String getPageContent(String url, CloseableHttpClient httpClient, HttpClientContext context) throws IOException {
 
         HttpGet request = new HttpGet(url);
 
@@ -179,23 +166,15 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
 
     }
 
-    private String parseAndCloseResponse(CloseableHttpResponse response) throws UnsupportedOperationException, IOException {
+    private String parseAndCloseResponse(CloseableHttpResponse response) {
         try {
             int responseCode = response.getStatusLine().getStatusCode();
+            String resp = EntityUtils.toString(response.getEntity());
+
             if (responseCode != 200) {
-                log.debug("Response Code : " + responseCode);
-            }
-            BufferedReader rd = new BufferedReader(
-                    new InputStreamReader(response.getEntity().getContent()));
-            StringBuilder result = new StringBuilder();
-            String line;
-            while ((line = rd.readLine()) != null) {
-                result.append(line);
-            }
-            if (responseCode != 200) {
-                log.debug(result.toString());
+                log.debugf("Response Code: %d, Body: %s", responseCode, resp);
             }
-            return result.toString();
+            return resp;
         } catch (IOException | UnsupportedOperationException ex) {
             throw new RuntimeException(ex);
         } finally {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
index d4de8e6..58193e9 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
@@ -55,6 +55,7 @@ import org.keycloak.testsuite.page.LoginPasswordUpdatePage;
 import org.keycloak.testsuite.pages.ErrorPage;
 import org.keycloak.testsuite.pages.InfoPage;
 import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.ProceedPage;
 import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
 import org.keycloak.testsuite.util.AdminEventPaths;
 import org.keycloak.testsuite.util.ClientBuilder;
@@ -108,6 +109,9 @@ public class UserTest extends AbstractAdminTest {
     protected InfoPage infoPage;
 
     @Page
+    protected ProceedPage proceedPage;
+
+    @Page
     protected ErrorPage errorPage;
 
     @Page
@@ -628,6 +632,9 @@ public class UserTest extends AbstractAdminTest {
 
         driver.navigate().to(link);
 
+        proceedPage.assertCurrent();
+        Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password"));
+        proceedPage.clickProceedLink();
         passwordUpdatePage.assertCurrent();
 
         passwordUpdatePage.changePassword("new-pass", "new-pass");
@@ -664,6 +671,9 @@ public class UserTest extends AbstractAdminTest {
 
             driver.navigate().to(link);
 
+            proceedPage.assertCurrent();
+            Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password"));
+            proceedPage.clickProceedLink();
             passwordUpdatePage.assertCurrent();
 
             passwordUpdatePage.changePassword("new-pass" + i, "new-pass" + i);
@@ -706,6 +716,9 @@ public class UserTest extends AbstractAdminTest {
 
             driver.navigate().to(link);
 
+            proceedPage.assertCurrent();
+            Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password"));
+            proceedPage.clickProceedLink();
             passwordUpdatePage.assertCurrent();
 
             passwordUpdatePage.changePassword("new-pass" + i, "new-pass" + i);
@@ -744,6 +757,9 @@ public class UserTest extends AbstractAdminTest {
 
         driver.navigate().to(link);
 
+        proceedPage.assertCurrent();
+        Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password"));
+        proceedPage.clickProceedLink();
         passwordUpdatePage.assertCurrent();
 
         driver.manage().deleteAllCookies();
@@ -751,6 +767,9 @@ public class UserTest extends AbstractAdminTest {
 
         driver.navigate().to(link);
 
+        proceedPage.assertCurrent();
+        Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password"));
+        proceedPage.clickProceedLink();
         passwordUpdatePage.assertCurrent();
 
         passwordUpdatePage.changePassword("new-pass", "new-pass");
@@ -850,6 +869,9 @@ public class UserTest extends AbstractAdminTest {
 
         driver.navigate().to(link);
 
+        proceedPage.assertCurrent();
+        Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password"));
+        proceedPage.clickProceedLink();
         passwordUpdatePage.assertCurrent();
 
         passwordUpdatePage.changePassword("new-pass", "new-pass");
@@ -910,6 +932,9 @@ public class UserTest extends AbstractAdminTest {
 
         driver.navigate().to(link);
 
+        proceedPage.assertCurrent();
+        Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password"));
+        proceedPage.clickProceedLink();
         passwordUpdatePage.assertCurrent();
 
         passwordUpdatePage.changePassword("new-pass", "new-pass");
@@ -981,11 +1006,17 @@ public class UserTest extends AbstractAdminTest {
 
         driver.navigate().to(link);
 
+        proceedPage.assertCurrent();
+        Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Verify Email"));
+        proceedPage.clickProceedLink();
         Assert.assertEquals("Your account has been updated.", infoPage.getInfo());
 
         driver.navigate().to("about:blank");
 
         driver.navigate().to(link); // It should be possible to use the same action token multiple times
+        proceedPage.assertCurrent();
+        Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Verify Email"));
+        proceedPage.clickProceedLink();
         Assert.assertEquals("Your account has been updated.", infoPage.getInfo());
     }
 
diff --git a/themes/src/main/resources/theme/base/account/messages/messages_en.properties b/themes/src/main/resources/theme/base/account/messages/messages_en.properties
index 454a934..47dbda1 100755
--- a/themes/src/main/resources/theme/base/account/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/account/messages/messages_en.properties
@@ -164,4 +164,5 @@ locale_no=Norsk
 locale_lt=Lietuvi\u0173
 locale_pt-BR=Portugu\u00EAs (Brasil)
 locale_ru=\u0420\u0443\u0441\u0441\u043A\u0438\u0439
-locale_zh-CN=\u4e2d\u6587\u7b80\u4f53
\ No newline at end of file
+locale_zh-CN=\u4e2d\u6587\u7b80\u4f53
+locale_sv=Svenska
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/email/html/executeActions.ftl b/themes/src/main/resources/theme/base/email/html/executeActions.ftl
index f75e10f..3af8d55 100755
--- a/themes/src/main/resources/theme/base/email/html/executeActions.ftl
+++ b/themes/src/main/resources/theme/base/email/html/executeActions.ftl
@@ -1,5 +1,8 @@
+<#assign requiredActionsText>
+<#if requiredActions??><#list requiredActions><b><#items as reqActionItem>${msg("requiredAction.${reqActionItem}")}<#sep>, </#items></b></#list><#else></#if>
+</#assign>
 <html>
 <body>
-${msg("executeActionsBodyHtml",link, linkExpiration, realmName)}
+${msg("executeActionsBodyHtml",link, linkExpiration, realmName, requiredActionsText)}
 </body>
 </html>
diff --git a/themes/src/main/resources/theme/base/email/messages/messages_en.properties b/themes/src/main/resources/theme/base/email/messages/messages_en.properties
index 8a0ae92..5cb1b6e 100755
--- a/themes/src/main/resources/theme/base/email/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/email/messages/messages_en.properties
@@ -11,8 +11,8 @@ passwordResetSubject=Reset password
 passwordResetBody=Someone just requested to change your {2} account''s credentials. If this was you, click on the link below to reset them.\n\n{0}\n\nThis link and code will expire within {1} minutes.\n\nIf you don''t want to reset your credentials, just ignore this message and nothing will be changed.
 passwordResetBodyHtml=<p>Someone just requested to change your {2} account''s credentials. If this was you, click on the link below to reset them.</p><p><a href="{0}">Link to reset credentials</a></p><p>This link will expire within {1} minutes.</p><p>If you don''t want to reset your credentials, just ignore this message and nothing will be changed.</p>
 executeActionsSubject=Update Your Account
-executeActionsBody=Your administrator has just requested that you update your {2} account. Click on the link below to start this process.\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you are unaware that your admin has requested this, just ignore this message and nothing will be changed.
-executeActionsBodyHtml=<p>Your administrator has just requested that you update your {2} account.  Click on the link below to start this process.</p><p><a href="{0}">Link to account update</a></p><p>This link will expire within {1} minutes.</p><p>If you are unaware that your admin has requested this, just ignore this message and nothing will be changed.</p>
+executeActionsBody=Your administrator has just requested that you update your {2} account by performing the following action(s): {3}. Click on the link below to start this process.\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you are unaware that your admin has requested this, just ignore this message and nothing will be changed.
+executeActionsBodyHtml=<p>Your administrator has just requested that you update your {2} account by performing the following action(s): {3}. Click on the link below to start this process.</p><p><a href="{0}">Link to account update</a></p><p>This link will expire within {1} minutes.</p><p>If you are unaware that your admin has requested this, just ignore this message and nothing will be changed.</p>
 eventLoginErrorSubject=Login error
 eventLoginErrorBody=A failed login attempt was detected to your account on {0} from {1}. If this was not you, please contact an admin.
 eventLoginErrorBodyHtml=<p>A failed login attempt was detected to your account on {0} from {1}. If this was not you, please contact an admin.</p>
@@ -25,3 +25,9 @@ eventUpdatePasswordBodyHtml=<p>Your password was changed on {0} from {1}. If thi
 eventUpdateTotpSubject=Update TOTP
 eventUpdateTotpBody=TOTP was updated for your account on {0} from {1}. If this was not you, please contact an admin.
 eventUpdateTotpBodyHtml=<p>TOTP was updated for your account on {0} from {1}. If this was not you, please contact an admin.</p>
+
+requiredAction.CONFIGURE_TOTP=Configure OTP
+requiredAction.terms_and_conditions=Terms and Conditions
+requiredAction.UPDATE_PASSWORD=Update Password
+requiredAction.UPDATE_PROFILE=Update Profile
+requiredAction.VERIFY_EMAIL=Verify Email
diff --git a/themes/src/main/resources/theme/base/email/text/executeActions.ftl b/themes/src/main/resources/theme/base/email/text/executeActions.ftl
index a33758f..39ce047 100755
--- a/themes/src/main/resources/theme/base/email/text/executeActions.ftl
+++ b/themes/src/main/resources/theme/base/email/text/executeActions.ftl
@@ -1 +1,4 @@
-${msg("executeActionsBody",link, linkExpiration, realmName)}
\ No newline at end of file
+<#assign requiredActionsText>
+<#if requiredActions??><#list requiredActions><#items as reqActionItem>${msg("requiredAction.${reqActionItem}")}<#sep>, </#items></#list><#else></#if>
+</#assign>
+${msg("executeActionsBody",link, linkExpiration, realmName, requiredActionsText)}
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/login/info.ftl b/themes/src/main/resources/theme/base/login/info.ftl
index cb228d2..c9e197b 100755
--- a/themes/src/main/resources/theme/base/login/info.ftl
+++ b/themes/src/main/resources/theme/base/login/info.ftl
@@ -6,11 +6,13 @@
     ${message.summary}
     <#elseif section = "form">
     <div id="kc-info-message">
-        <p class="instruction">${message.summary}</p>
+        <p class="instruction">${message.summary}<#if requiredActions??><#list requiredActions>: <b><#items as reqActionItem>${msg("requiredAction.${reqActionItem}")}<#sep>, </#items></b></#list><#else></#if></p>
         <#if skipLink??>
         <#else>
             <#if pageRedirectUri??>
                 <p><a href="${pageRedirectUri}">${msg("backToApplication")}</a></p>
+            <#elseif actionUri??>
+                <p><a href="${actionUri}">${msg("proceedWithAction")}</a></p>
             <#elseif client.baseUrl??>
                 <p><a href="${client.baseUrl}">${msg("backToApplication")}</a></p>
             </#if>
diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
index 947b64d..dbd0a3c 100755
--- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
@@ -219,6 +219,9 @@ identityProviderNotUniqueMessage=Realm supports multiple identity providers. Cou
 emailVerifiedMessage=Your email address has been verified.
 staleEmailVerificationLink=The link you clicked is a old stale link and is no longer valid.  Maybe you have already verified your email?
 identityProviderAlreadyLinkedMessage=Federated identity returned by {0} is already linked to another user.
+confirmAccountLinking=Confirm linking the account {0} of identity provider {1} with your account.
+confirmEmailAddressVerification=Confirm validity of e-mail address {0}.
+confirmExecutionOfActions=Perform the following action(s)
 
 locale_ca=Catal\u00E0
 locale_de=Deutsch
@@ -233,6 +236,7 @@ locale_pt-BR=Portugu\u00EAs (Brasil)
 locale_ru=\u0420\u0443\u0441\u0441\u043A\u0438\u0439
 locale_lt=Lietuvi\u0173
 locale_zh-CN=\u4e2d\u6587\u7b80\u4f53
+locale_sv=Svenska
 
 backToApplication=&laquo; Back to Application
 missingParameterMessage=Missing parameters\: {0}
@@ -242,5 +246,12 @@ invalidParameterMessage=Invalid parameter\: {0}
 alreadyLoggedIn=You are already logged in.
 differentUserAuthenticated=You are already authenticated as different user ''{0}'' in this session. Please logout first.
 brokerLinkingSessionExpired=Requested broker account linking, but current session is no longer valid.
+proceedWithAction=&raquo; Click here to proceed
+
+requiredAction.CONFIGURE_TOTP=Configure OTP
+requiredAction.terms_and_conditions=Terms and Conditions
+requiredAction.UPDATE_PASSWORD=Update Password
+requiredAction.UPDATE_PROFILE=Update Profile
+requiredAction.VERIFY_EMAIL=Verify Email
 
 p3pPolicy=CP="This is not a P3P policy!"
diff --git a/themes/src/main/resources-community/theme/base/account/messages/messages_sv.properties b/themes/src/main/resources-community/theme/base/account/messages/messages_sv.properties
index 8c83abb..cc134cd 100755
--- a/themes/src/main/resources-community/theme/base/account/messages/messages_sv.properties
+++ b/themes/src/main/resources-community/theme/base/account/messages/messages_sv.properties
@@ -2,14 +2,14 @@
 doSave=Spara
 doCancel=Avbryt
 doLogOutAllSessions=Logga ut från samtliga sessioner
-doRemove=Ta Bort
-doAdd=Lägg Till
-doSignOut=Logga Ut
+doRemove=Ta bort
+doAdd=Lägg till
+doSignOut=Logga ut
 
-editAccountHtmlTitle=Redigera Konto
+editAccountHtmlTitle=Redigera konto
 federatedIdentitiesHtmlTitle=Federerade identiteter
-accountLogHtmlTitle=Kontoslogg
-changePasswordHtmlTitle=Byt Lösenord
+accountLogHtmlTitle=Kontologg
+changePasswordHtmlTitle=Byt lösenord
 sessionsHtmlTitle=Sessioner
 accountManagementTitle=Kontohantering för Keycloak
 authenticatorTitle=Autentiserare
@@ -21,40 +21,40 @@ firstName=Förnamn
 lastName=Efternamn
 password=Lösenord
 passwordConfirm=Bekräftelse
-passwordNew=Nytt Lösenord
+passwordNew=Nytt lösenord
 username=Användarnamn
 address=Adress
 street=Gata
 locality=Postort
-region=Stat, Provins, eller Region
+region=Stat, Provins eller Region
 postal_code=Postnummer
 country=Land
 emailVerified=E-post verifierad
 gssDelegationCredential=GSS Delegation Credential
 
 role_admin=Administratör
-role_realm-admin=Realm Administratör
+role_realm-admin=Realm-administratör
 role_create-realm=Skapa realm
 role_view-realm=Visa realm
 role_view-users=Visa användare
 role_view-applications=Visa applikationer
 role_view-clients=Visa klienter
 role_view-events=Visa event
-role_view-identity-providers=Visa identity providers
+role_view-identity-providers=Visa identitetsleverantörer
 role_manage-realm=Hantera realm
 role_manage-users=Hantera användare
 role_manage-applications=Hantera applikationer
-role_manage-identity-providers=Hantera identity providers
+role_manage-identity-providers=Hantera identitetsleverantörer
 role_manage-clients=Hantera klienter
 role_manage-events=Hantera event
 role_view-profile=Visa profil
 role_manage-account=Hantera konto
 role_read-token=Läs element
-role_offline-access=Åtkomst Offline
+role_offline-access=Åtkomst offline
 role_uma_authorization=Erhåll tillstånd
 client_account=Konto
 client_security-admin-console=Säkerhetsadministratörskonsol
-client_admin-cli=Administratörs CLI
+client_admin-cli=Administratörs-CLI
 client_realm-management=Realmhantering
 
 
@@ -71,7 +71,7 @@ client=Klient
 clients=Klienter
 details=Detaljer
 started=Startade
-lastAccess=Senast Åtkomst
+lastAccess=Senast åtkomst
 expires=Upphör
 applications=Applikationer
 
@@ -82,20 +82,20 @@ sessions=Sessioner
 log=Logg
 
 application=Applikation
-availablePermissions=Tillgängliga Tillstånd
-grantedPermissions=Beviljade Tillstånd
-grantedPersonalInfo=Medgiven Personlig Information
-additionalGrants=Ytterligare Medgivanden
+availablePermissions=Tillgängliga rättigheter
+grantedPermissions=Beviljade rättigheter
+grantedPersonalInfo=Medgiven personlig information
+additionalGrants=Ytterligare medgivanden
 action=Åtgärd
-inResource=in
-fullAccess=Fullständig Åtkomst
-offlineToken=Offline Token
-revoke=Upphäv Tillstånd
+inResource=i
+fullAccess=Fullständig åtkomst
+offlineToken=Offline token
+revoke=Upphäv rättighet
 
-configureAuthenticators=Ändrade Autentiserare
+configureAuthenticators=Konfigurerade autentiserare
 mobile=Mobil
 totpStep1=Installera <a href="https://freeotp.github.io/" target="_blank">FreeOTP</a> eller Google Authenticator på din enhet. Båda applikationerna finns tillgängliga på <a href="https://play.google.com">Google Play</a> och Apple App Store.
-totpStep2=Öppna applikationen och skanna streckkoden eller skriv i nyckeLn.
+totpStep2=Öppna applikationen och skanna streckkoden eller skriv i nyckeln.
 totpStep3=Fyll i engångskoden som tillhandahålls av applikationen och klicka på Spara för att avsluta inställningarna.
 
 missingUsernameMessage=Vänligen ange användarnamn.
@@ -106,10 +106,10 @@ missingEmailMessage=Vänligen ange e-post.
 missingPasswordMessage=Vänligen ange lösenord.
 notMatchPasswordMessage=Lösenorden matchar inte.
 
-missingTotpMessage=Vänligen ange autentiserarekoden.
+missingTotpMessage=Vänligen ange autentiseringskoden.
 invalidPasswordExistingMessage=Det nuvarande lösenordet är ogiltigt.
 invalidPasswordConfirmMessage=Lösenordsbekräftelsen matchar inte.
-invalidTotpMessage=Autentiserarekoden är ogiltig.
+invalidTotpMessage=Autentiseringskoden är ogiltig.
 
 usernameExistsMessage=Användarnamnet finns redan.
 emailExistsMessage=E-posten finns redan.
@@ -120,20 +120,20 @@ readOnlyPasswordMessage=Du kan inte uppdatera ditt lösenord eftersom ditt konto
 successTotpMessage=Mobilautentiseraren är inställd.
 successTotpRemovedMessage=Mobilautentiseraren är borttagen.
 
-successGrantRevokedMessage=Upphävandet av tillståndet lyckades.
+successGrantRevokedMessage=Upphävandet av rättigheten lyckades.
 
 accountUpdatedMessage=Ditt konto har uppdaterats.
 accountPasswordUpdatedMessage=Ditt lösenord har uppdaterats.
 
-missingIdentityProviderMessage=Identity provider är inte angiven.
+missingIdentityProviderMessage=Identitetsleverantör är inte angiven.
 invalidFederatedIdentityActionMessage=Åtgärden är ogiltig eller saknas.
-identityProviderNotFoundMessage=Angiven identity provider hittas inte.
+identityProviderNotFoundMessage=Angiven identitetsleverantör hittas inte.
 federatedIdentityLinkNotActiveMessage=Den här identiteten är inte längre aktiv.
-federatedIdentityRemovingLastProviderMessage=Du kan inte ta bort senaste federerade identiteten eftersom du inte har lösenordet.
-identityProviderRedirectErrorMessage=Misslyckades med att omdirigera till identity provider.
-identityProviderRemovedMessage=Borttaginingen av Identity provider lyckades.
+federatedIdentityRemovingLastProviderMessage=Du kan inte ta bort senaste federerade identiteten eftersom du inte har ett lösenord.
+identityProviderRedirectErrorMessage=Misslyckades med att omdirigera till identitetsleverantör.
+identityProviderRemovedMessage=Borttagningen av identitetsleverantören lyckades.
 identityProviderAlreadyLinkedMessage=Den federerade identiteten  som returnerades av {0} är redan länkad till en annan användare.
-staleCodeAccountMessage=Sidan har redan upphört. Vänligen försök igen.
+staleCodeAccountMessage=Sidan har upphört att gälla. Vänligen försök igen.
 consentDenied=Samtycket förnekades.
 
 accountDisabledMessage=Kontot är inaktiverat, kontakta administratör.
@@ -145,6 +145,6 @@ invalidPasswordMinDigitsMessage=Ogiltigt lösenord: måste innehålla minst {0} 
 invalidPasswordMinUpperCaseCharsMessage=Ogiltigt lösenord: måste innehålla minst {0} stora bokstäver.
 invalidPasswordMinSpecialCharsMessage=Ogiltigt lösenord: måste innehålla minst {0} specialtecken.
 invalidPasswordNotUsernameMessage=Ogiltigt lösenord: Får inte vara samma som användarnamnet.
-invalidPasswordRegexPatternMessage=Ogiltigt lösenord: matchar inte regex mönstret(en).
+invalidPasswordRegexPatternMessage=Ogiltigt lösenord: matchar inte kravet för lösenordsmönster.
 invalidPasswordHistoryMessage=Ogiltigt lösenord: Får inte vara samma som de senaste {0} lösenorden.
 invalidPasswordGenericMessage=Ogiltigt lösenord: Det nya lösenordet stämmer inte med lösenordspolicyn.
\ No newline at end of file
diff --git a/themes/src/main/resources-community/theme/base/email/messages/messages_sv.properties b/themes/src/main/resources-community/theme/base/email/messages/messages_sv.properties
index 5b5ac6d..a5ffbf4 100755
--- a/themes/src/main/resources-community/theme/base/email/messages/messages_sv.properties
+++ b/themes/src/main/resources-community/theme/base/email/messages/messages_sv.properties
@@ -6,9 +6,9 @@ identityProviderLinkSubject=Länk {0}
 identityProviderLinkBody=Någon vill länka ditt "{1}" konto med "{0}" kontot tillhörande användaren {2} . Om det var du, klicka då på länken nedan för att länka kontona\n\n{3}\n\nDen här länken kommer att upphöra inom {4} minuter.\n\nOm du inte vill länka kontot, ignorera i så fall det här meddelandet. Om du länkar kontona, så kan du logga in till {1} genom {0}.
 identityProviderLinkBodyHtml=<p>Någon vill länka ditt <b>{1}</b> konto med <b>{0}</b> kontot tillhörande användaren {2} . Om det var du, klicka då på länken nedan för att länka kontona</p><p><a href="{3}">{3}</a></p><p>Den här länken kommer att upphöra inom {4} minuter.</p><p>Om du inte vill länka kontot, ignorera i så fall det här meddelandet. Om du länkar kontona, så kan du logga in till {1} genom {0}.</p>
 passwordResetSubject=Återställ lösenord
-passwordResetBody=Någon har precis bett om att ändra ditt {2} kontos användaruppgifter. Om det var du, klicka då på länken nedan för att återställa dem.\n\n{0}\n\nDen här länken och koden kommer att upphöra inom {1} minuter.\n\nOm du inte vill återställa dina kontouppgifter, ignorera i så fall det här meddelandet så kommer inget att ändras.
-passwordResetBodyHtml=<p>Någon har precis bett om att ändra ditt {2} kontos användaruppgifter. Om det var du, klicka då på länken nedan för att återställa dem.</p><p><a href="{0}">{0}</a></p><p>Den här länken och koden kommer att upphöra inom {1} minuter.</p><p>Om du inte vill återställa dina kontouppgifter, ignorera i så fall det här meddelandet så kommer inget att ändras.</p>
-executeActionsSubject=Uppdatera Ditt Konto
+passwordResetBody=Någon har precis bett om att ändra användaruppgifter för ditt konto {2}. Om det var du, klicka då på länken nedan för att återställa dem.\n\n{0}\n\nDen här länken och koden kommer att upphöra inom {1} minuter.\n\nOm du inte vill återställa dina kontouppgifter, ignorera i så fall det här meddelandet så kommer inget att ändras.
+passwordResetBodyHtml=<p>Någon har precis bett om att ändra användaruppgifter för ditt konto {2}. Om det var du, klicka då på länken nedan för att återställa dem.</p><p><a href="{0}">{0}</a></p><p>Den här länken och koden kommer att upphöra inom {1} minuter.</p><p>Om du inte vill återställa dina kontouppgifter, ignorera i så fall det här meddelandet så kommer inget att ändras.</p>
+executeActionsSubject=Uppdatera ditt konto
 executeActionsBody=Din administratör har precis bett om att du skall uppdatera ditt {2} konto. Klicka på länken för att påbörja processen.\n\n{0}\n\nDen här länken kommer att upphöra inom {1} minuter.\n\nOm du är omedveten om att din administratör har bett om detta, ignorera i så fall det här meddelandet så kommer inget att ändras.
 executeActionsBodyHtml=<p>Din administratör har precis bett om att du skall uppdatera ditt {2} konto. Klicka på länken för att påbörja processen.</p><p><a href="{0}">{0}</a></p><p>Den här länken kommer att upphöra inom {1} minuter.</p><p>Om du är omedveten om att din administratör har bett om detta, ignorera i så fall det här meddelandet så kommer inget att ändras.</p>
 eventLoginErrorSubject=Inloggningsfel
diff --git a/themes/src/main/resources-community/theme/base/login/messages/messages_sv.properties b/themes/src/main/resources-community/theme/base/login/messages/messages_sv.properties
index c671ee3..c383703 100755
--- a/themes/src/main/resources-community/theme/base/login/messages/messages_sv.properties
+++ b/themes/src/main/resources-community/theme/base/login/messages/messages_sv.properties
@@ -1,5 +1,5 @@
 # encoding: utf-8
-doLogIn=Logga In
+doLogIn=Logga in
 doRegister=Registrera
 doCancel=Avbryt
 doSubmit=Skicka
@@ -8,30 +8,30 @@ doNo=Nej
 doContinue=Fortsätt
 doAccept=Acceptera
 doDecline=Avböj
-doForgotPassword=Glömt Lösenord?
+doForgotPassword=Glömt lösenord?
 doClickHere=Klicka här
 doImpersonate=Imitera
-kerberosNotConfigured=Kerberos är Inte Konfigurerad
-kerberosNotConfiguredTitle=Kerberos är Inte Konfigurerad
-bypassKerberosDetail=Antingen så är du inte inloggad via Kerberos eller så är inte din webläsare inställd för Kerberosinloggning. Vänligen klicka på fortsätt för att logga in på annat sätt.
+kerberosNotConfigured=Kerberos är inte konfigurerat
+kerberosNotConfiguredTitle=Kerberos är inte konfigurerat
+bypassKerberosDetail=Antingen så är du inte inloggad via Kerberos eller så är inte din webbläsare inställd för Kerberosinloggning. Vänligen klicka på fortsätt för att logga in på annat sätt.
 kerberosNotSetUp=Kerberos är inte inställt. Du kan inte logga in.
 registerWithTitle=Registrera med {0}
 registerWithTitleHtml={0}
 loginTitle=Logga in till {0}
 loginTitleHtml={0}
-impersonateTitle={0} Imitera Användare
-impersonateTitleHtml=<strong>{0}</strong> Imitera Användare</strong>
+impersonateTitle={0} Imitera användare
+impersonateTitleHtml=<strong>{0}</strong> Imitera användare</strong>
 realmChoice=Realm
 unknownUser=Okänd användare
-loginTotpTitle=Inställning av Mobilautentiserare
-loginProfileTitle=Uppdatera Kontoinformation
-loginTimeout=Du tog för lång tid för att logga in. Inloggningsprocessen börjar om.
-oauthGrantTitle=Bevilja Åtkomst
+loginTotpTitle=Inställning av mobilautentiserare
+loginProfileTitle=Uppdatera kontoinformation
+loginTimeout=Det tog för lång tid att logga in. Inloggningsprocessen börjar om.
+oauthGrantTitle=Bevilja åtkomst
 oauthGrantTitleHtml={0}
 errorTitle=Vi ber om ursäkt...
 errorTitleHtml=Vi ber om <strong>ursäkt</strong> ...
-emailVerifyTitle=E-postsverifikation
-emailForgotTitle=Glömt Ditt Lösenord?
+emailVerifyTitle=E-postverifiering
+emailForgotTitle=Glömt ditt lösenord?
 updatePasswordTitle=Uppdatera lösenord
 codeSuccessTitle=Rätt kod
 codeErrorTitle=Felkod\: {0}
@@ -51,15 +51,15 @@ firstName=Förnamn
 lastName=Efternamn
 email=E-post
 password=Lösenord
-passwordConfirm=Bekräfta Lösenord
-passwordNew=Nytt Lösenord
-passwordNewConfirm=Bekräftelse av Nytt Lösenord
+passwordConfirm=Bekräfta lösenord
+passwordNew=Nytt lösenord
+passwordNewConfirm=Bekräftelse av nytt lösenord
 rememberMe=Kom ihåg mig
 authenticatorCode=Engångskod
 address=Adress
 street=Gata
 locality=Postort
-region=Stat, Provins, eller Region
+region=Stat, Provins eller Region
 postal_code=Postnummer
 country=Land
 emailVerified=E-post verifierad
@@ -70,8 +70,8 @@ loginTotpStep2=Öppna applikationen och skanna streckkoden eller skriv i nyckeln
 loginTotpStep3=Fyll i engångskoden som tillhandahålls av applikationen och klicka på Spara för att avsluta inställningarna
 loginTotpOneTime=Engångskod
 
-oauthGrantRequest=Godkänner du dessa åtkomstförmånen?
-inResource=in
+oauthGrantRequest=Godkänner du tillgång till de här rättigheterna?
+inResource=i
 
 emailVerifyInstruction1=Ett e-postmeddelande med instruktioner om hur du verifierar din e-postadress har skickats till dig.
 emailVerifyInstruction2=Har du inte fått en verifikationskod i din e-post?
@@ -88,9 +88,9 @@ emailInstruction=Fyll i ditt användarnamn eller din e-postadress, så kommer vi
 
 copyCodeInstruction=Vänligen kopiera den här koden och klistra in den i din applikation:
 
-personalInfo=Personlig Information:
+personalInfo=Personlig information:
 role_admin=Administratör
-role_realm-admin=Realm Administratör
+role_realm-admin=Realm-administratör
 role_create-realm=Skapa realm
 role_create-client=Skapa klient
 role_view-realm=Visa realm
@@ -98,39 +98,39 @@ role_view-users=Visa användare
 role_view-applications=Visa applikationer
 role_view-clients=Visa klienter
 role_view-events=Visa event
-role_view-identity-providers=Visa identity providers
+role_view-identity-providers=Visa identitetsleverantörer
 role_manage-realm=Hantera realm
 role_manage-users=Hantera användare
 role_manage-applications=Hantera applikationer
-role_manage-identity-providers=Hantera identity providers
+role_manage-identity-providers=Hantera identitetsleverantörer
 role_manage-clients=Hantera klienter
 role_manage-events=Hantera event
 role_view-profile=Visa profil
 role_manage-account=Hantera konto
 role_read-token=Läs element
-role_offline-access=Åtkomst Offline
+role_offline-access=Åtkomst offline
 client_account=Konto
 client_security-admin-console=Säkerhetsadministratörskonsol
-client_admin-cli=Administratörs CLI
+client_admin-cli=Administratörs-CLI
 client_realm-management=Realmhantering
 
 invalidUserMessage=Ogiltigt användarnamn eller lösenord.
 invalidEmailMessage=Ogiltig e-postadress.
 accountDisabledMessage=Kontot är inaktiverat, kontakta administratör.
 accountTemporarilyDisabledMessage=Kontot är tillfälligt inaktiverat, kontakta administratör eller försök igen senare.
-expiredCodeMessage=Inloggnings time-out. Vänligen försök igen.
+expiredCodeMessage=Inloggningen nådde en maxtidsgräns. Vänligen försök igen.
 
 missingFirstNameMessage=Vänligen ange förnamn.
 missingLastNameMessage=Vänligen ange efternamn.
 missingEmailMessage=Vänligen ange e-post.
 missingUsernameMessage=Vänligen ange användarnamn.
 missingPasswordMessage=Vänligen ange lösenord.
-missingTotpMessage=Vänligen ange autentiserarekod.
+missingTotpMessage=Vänligen ange autentiseringskod.
 notMatchPasswordMessage=Lösenorden matchar inte.
 
 invalidPasswordExistingMessage=Det nuvarande lösenordet är ogiltigt.
 invalidPasswordConfirmMessage=Lösenordsbekräftelsen matchar inte.
-invalidTotpMessage=Autentiserarekoden är ogiltig.
+invalidTotpMessage=Autentiseringskoden är ogiltig.
 
 usernameExistsMessage=Användarnamnet finns redan.
 emailExistsMessage=E-postadressen finns redan.
@@ -169,42 +169,42 @@ invalidPasswordGenericMessage=Ogiltigt lösenord: Det nya lösenordet stämmer i
 
 failedToProcessResponseMessage=Misslyckades med att behandla svaret
 httpsRequiredMessage=HTTPS krävs
-realmNotEnabledMessage=Realm är inte aktiverat
-invalidRequestMessage=Ogiltig Förfrågan
+realmNotEnabledMessage=Realm är inte aktiverad
+invalidRequestMessage=Ogiltig förfrågan
 failedLogout=Utloggning misslyckades
 unknownLoginRequesterMessage=Okänd inloggningsförfrågan
 loginRequesterNotEnabledMessage=Inloggningsförfrågaren är inte aktiverad
-bearerOnlyMessage=Bearer-only applikationer tillåts inte att initiera inloggning genom webbläsare
+bearerOnlyMessage=Bearer-only-applikationer tillåts inte att initiera inloggning genom webbläsare
 standardFlowDisabledMessage=Klienten tillåts inte att initiera inloggning genom webbläsare med det givna response_type. Standardflödet är inaktiverat för klienten.
 implicitFlowDisabledMessage=Klienten tillåts inte att initiera inloggning genom webbläsare med det givna response_type. Villkorslöst flöde är inaktiverat för klienten.
-invalidRedirectUriMessage=Ogiltig omdirigerad uri
+invalidRedirectUriMessage=Ogiltig omdirigeringsadress
 unsupportedNameIdFormatMessage=NameIDFormat stöds ej
 invalidRequesterMessage=Ogiltig förfrågare
 registrationNotAllowedMessage=Registrering tillåts ej
 resetCredentialNotAllowedMessage=Återställning av uppgifter tillåts ej
 
-permissionNotApprovedMessage=Tillståndet ej godkänt.
-noRelayStateInResponseMessage=Inget vidarebefordrat tillstånd i svaret från identity provider.
+permissionNotApprovedMessage=Rättigheten ej godkänd.
+noRelayStateInResponseMessage=Inget vidarebefordrat tillstånd i svaret från identitetsleverantör.
 insufficientPermissionMessage=Otillräckliga tillstånd för att länka identiteter.
-couldNotProceedWithAuthenticationRequestMessage=Kunde inte fortsätta med autentiseringsförfrågan till identity provider.
-couldNotObtainTokenMessage=Kunde inte motta element från identity provider.
-unexpectedErrorRetrievingTokenMessage=Oväntat fel när element hämtas från identity provider.
-unexpectedErrorHandlingResponseMessage=Oväntat fel under hantering av svar från från identity provider.
-identityProviderAuthenticationFailedMessage=Autentiseringen misslyckades. Kunde inte autentisera med identity provider.
+couldNotProceedWithAuthenticationRequestMessage=Kunde inte fortsätta med autentiseringsförfrågan till identitetsleverantör.
+couldNotObtainTokenMessage=Kunde inte motta element från identitetsleverantör.
+unexpectedErrorRetrievingTokenMessage=Oväntat fel när element hämtas från identitetsleverantör.
+unexpectedErrorHandlingResponseMessage=Oväntat fel under hantering av svar från från identitetsleverantör.
+identityProviderAuthenticationFailedMessage=Autentiseringen misslyckades. Kunde inte autentisera med identitetsleverantör.
 identityProviderDifferentUserMessage=Autentiserad som {0}, men väntades att vara autentiserad som {1}
-couldNotSendAuthenticationRequestMessage=Kunde inte skicka autentiseringsförfrågan till identity provider.
-unexpectedErrorHandlingRequestMessage=Oväntat fel under hantering av autentiseringsförfrågan till identity provider.
+couldNotSendAuthenticationRequestMessage=Kunde inte skicka autentiseringsförfrågan till identitetsleverantör.
+unexpectedErrorHandlingRequestMessage=Oväntat fel under hantering av autentiseringsförfrågan till identitetsleverantör.
 invalidAccessCodeMessage=Ogiltig tillträdeskod.
 sessionNotActiveMessage=Sessionen ej aktiv.
 invalidCodeMessage=Ett fel uppstod, vänligen logga in igen genom din applikation.
-identityProviderUnexpectedErrorMessage=Oväntat fel under autentiseringen med identity provider
-identityProviderNotFoundMessage=Kunde inte hitta en identity provider med identifikatorn.
-identityProviderLinkSuccess=Ditt konto lyckades med att länka {0} med kontot {1} .
+identityProviderUnexpectedErrorMessage=Oväntat fel under autentiseringen med identitetsleverantör
+identityProviderNotFoundMessage=Kunde inte hitta en identitetsleverantör med identifikatorn.
+identityProviderLinkSuccess=Ditt konto lyckades med att länka {0} med kontot {1}.
 staleCodeMessage=Den här sidan är inte längre giltig, vänligen gå tillbaka till din applikation och logga in igen
-realmSupportsNoCredentialsMessage=Realm:et stödjer inga inloggningstyper.
-identityProviderNotUniqueMessage=Realm:et stödjer flera identity providers. Kunde inte avgöra vilken identity provider som skall användas för autentisering.
+realmSupportsNoCredentialsMessage=Realmen stödjer inga inloggningstyper.
+identityProviderNotUniqueMessage=Realmen stödjer flera identitetsleverantör. Kunde inte avgöra vilken identitetsleverantör som skall användas för autentisering.
 emailVerifiedMessage=Din e-postadress har blivit verifierad.
-staleEmailVerificationLink=Länken du klickade på är en gammal inaktuell länk som inte längre är giltig. Kanske har du redan verifierat din e-post?
+staleEmailVerificationLink=Länken du klickade på är en gammal, inaktuell länk som inte längre är giltig. Kanske har du redan verifierat din e-post?
 
 backToApplication=&laquo; Tillbaka till applikationen
 missingParameterMessage=Parametrar som saknas\: {0}