keycloak-aplcache
Changes
model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java 19(+9 -10)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStore.java 2(+1 -1)
model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheClientListenersTest.java 295(+295 -0)
model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java 9(+5 -4)
model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGSessionsCacheTest.java 51(+36 -15)
services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java 21(+21 -0)
services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java 12(+12 -0)
services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java 34(+27 -7)
services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java 12(+12 -0)
services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java 31(+27 -4)
services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java 2(+1 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java 9(+9 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java 3(+3 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/ProceedPage.java 51(+51 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java 9(+9 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java 114(+55 -59)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrencyTest.java 339(+169 -170)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java 105(+42 -63)
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=« 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=» 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=« Tillbaka till applikationen
missingParameterMessage=Parametrar som saknas\: {0}