keycloak-aplcache

KEYCLOAK-4627 IdP email account verification + code cleanup.

5/2/2017 1:18:32 PM

Changes

Details

diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java
new file mode 100644
index 0000000..d7bdcdf
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java
@@ -0,0 +1,53 @@
+/*
+ * 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.models.cache.infinispan.events;
+
+import org.keycloak.cluster.ClusterEvent;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
+
+    private String authSessionId;
+
+    private Map<String, String> authNotesFragment;
+
+    public static AuthenticationSessionAuthNoteUpdateEvent create(String authSessionId, Map<String, String> authNotesFragment) {
+        AuthenticationSessionAuthNoteUpdateEvent event = new AuthenticationSessionAuthNoteUpdateEvent();
+        event.authSessionId = authSessionId;
+        event.authNotesFragment = new LinkedHashMap<>(authNotesFragment);
+        return event;
+    }
+
+    public String getAuthSessionId() {
+        return authSessionId;
+    }
+
+    public Map<String, String> getAuthNotesFragment() {
+        return authNotesFragment;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("AuthenticationSessionAuthNoteUpdateEvent [ authSessionId=%s, authNotesFragment=%s ]", authSessionId, authNotesFragment);
+    }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java
index 202fe5c..05a762b 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java
@@ -18,7 +18,7 @@
 package org.keycloak.models.sessions.infinispan;
 
 import java.util.Collections;
-import java.util.HashMap;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
@@ -144,21 +144,27 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel 
 
     @Override
     public String getClientNote(String name) {
-        return entity.getClientNotes() != null ? entity.getClientNotes().get(name) : null;
+        return (entity.getClientNotes() != null && name != null) ? entity.getClientNotes().get(name) : null;
     }
 
     @Override
     public void setClientNote(String name, String value) {
         if (entity.getClientNotes() == null) {
-            entity.setClientNotes(new HashMap<>());
+            entity.setClientNotes(new ConcurrentHashMap<>());
+        }
+        if (name != null) {
+            if (value == null) {
+                entity.getClientNotes().remove(name);
+            } else {
+                entity.getClientNotes().put(name, value);
+            }
         }
-        entity.getClientNotes().put(name, value);
         update();
     }
 
     @Override
     public void removeClientNote(String name) {
-        if (entity.getClientNotes() != null) {
+        if (entity.getClientNotes() != null && name != null) {
             entity.getClientNotes().remove(name);
         }
         update();
@@ -167,34 +173,40 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel 
     @Override
     public Map<String, String> getClientNotes() {
         if (entity.getClientNotes() == null || entity.getClientNotes().isEmpty()) return Collections.emptyMap();
-        Map<String, String> copy = new HashMap<>();
+        Map<String, String> copy = new ConcurrentHashMap<>();
         copy.putAll(entity.getClientNotes());
         return copy;
     }
 
     @Override
     public void clearClientNotes() {
-        entity.setClientNotes(new HashMap<>());
+        entity.setClientNotes(new ConcurrentHashMap<>());
         update();
     }
 
     @Override
     public String getAuthNote(String name) {
-        return entity.getAuthNotes() != null ? entity.getAuthNotes().get(name) : null;
+        return (entity.getAuthNotes() != null && name != null) ? entity.getAuthNotes().get(name) : null;
     }
 
     @Override
     public void setAuthNote(String name, String value) {
         if (entity.getAuthNotes() == null) {
-            entity.setAuthNotes(new HashMap<String, String>());
+            entity.setAuthNotes(new ConcurrentHashMap<>());
+        }
+        if (name != null) {
+            if (value == null) {
+                entity.getAuthNotes().remove(name);
+            } else {
+                entity.getAuthNotes().put(name, value);
+            }
         }
-        entity.getAuthNotes().put(name, value);
         update();
     }
 
     @Override
     public void removeAuthNote(String name) {
-        if (entity.getAuthNotes() != null) {
+        if (entity.getAuthNotes() != null && name != null) {
             entity.getAuthNotes().remove(name);
         }
         update();
@@ -202,16 +214,22 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel 
 
     @Override
     public void clearAuthNotes() {
-        entity.setAuthNotes(new HashMap<>());
+        entity.setAuthNotes(new ConcurrentHashMap<>());
         update();
     }
 
     @Override
     public void setUserSessionNote(String name, String value) {
         if (entity.getUserSessionNotes() == null) {
-            entity.setUserSessionNotes(new HashMap<String, String>());
+            entity.setUserSessionNotes(new ConcurrentHashMap<>());
+        }
+        if (name != null) {
+            if (value == null) {
+                entity.getUserSessionNotes().remove(name);
+            } else {
+                entity.getUserSessionNotes().put(name, value);
+            }
         }
-        entity.getUserSessionNotes().put(name, value);
         update();
 
     }
@@ -221,14 +239,14 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel 
         if (entity.getUserSessionNotes() == null) {
             return Collections.EMPTY_MAP;
         }
-        HashMap<String, String> copy = new HashMap<>();
+        ConcurrentHashMap<String, String> copy = new ConcurrentHashMap<>();
         copy.putAll(entity.getUserSessionNotes());
         return copy;
     }
 
     @Override
     public void clearUserSessionNotes() {
-        entity.setUserSessionNotes(new HashMap<String, String>());
+        entity.setUserSessionNotes(new ConcurrentHashMap<>());
         update();
 
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java
index a802544..5991f98 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java
@@ -17,6 +17,7 @@
 
 package org.keycloak.models.sessions.infinispan;
 
+import org.keycloak.cluster.ClusterProvider;
 import java.util.Iterator;
 import java.util.Map;
 
@@ -27,6 +28,7 @@ import org.keycloak.common.util.Time;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
+import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent;
 import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
 import org.keycloak.models.sessions.infinispan.stream.AuthenticationSessionPredicate;
 import org.keycloak.models.utils.KeycloakModelUtils;
@@ -139,6 +141,20 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
     }
 
     @Override
+    public void updateNonlocalSessionAuthNotes(String authSessionId, Map<String, String> authNotesFragment) {
+        if (authSessionId == null) {
+            return;
+        }
+
+        ClusterProvider cluster = session.getProvider(ClusterProvider.class);
+        cluster.notify(
+          InfinispanAuthenticationSessionProviderFactory.AUTHENTICATION_SESSION_EVENTS,
+          AuthenticationSessionAuthNoteUpdateEvent.create(authSessionId, authNotesFragment),
+          true
+        );
+    }
+
+    @Override
     public void close() {
 
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
index e6da14e..aa6ede3 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
@@ -19,18 +19,28 @@ package org.keycloak.models.sessions.infinispan;
 
 import org.infinispan.Cache;
 import org.keycloak.Config;
+import org.keycloak.cluster.ClusterEvent;
+import org.keycloak.cluster.ClusterProvider;
 import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent;
 import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
 import org.keycloak.sessions.AuthenticationSessionProvider;
 import org.keycloak.sessions.AuthenticationSessionProviderFactory;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+import org.jboss.logging.Logger;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
 public class InfinispanAuthenticationSessionProviderFactory implements AuthenticationSessionProviderFactory {
 
+    private static final Logger log = Logger.getLogger(InfinispanAuthenticationSessionProviderFactory.class);
+
+    private volatile Cache<String, AuthenticationSessionEntity> authSessionsCache;
 
     @Override
     public void init(Config.Scope config) {
@@ -39,12 +49,53 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
 
     @Override
     public AuthenticationSessionProvider create(KeycloakSession session) {
-        InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
-        Cache<String, AuthenticationSessionEntity> authSessionsCache = connections.getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME);
-
+        lazyInit(session);
         return new InfinispanAuthenticationSessionProvider(session, authSessionsCache);
     }
 
+    private void updateAuthNotes(ClusterEvent clEvent) {
+        if (! (clEvent instanceof AuthenticationSessionAuthNoteUpdateEvent)) {
+            return;
+        }
+
+        AuthenticationSessionAuthNoteUpdateEvent event = (AuthenticationSessionAuthNoteUpdateEvent) clEvent;
+        AuthenticationSessionEntity authSession = this.authSessionsCache.get(event.getAuthSessionId());
+        updateAuthSession(authSession, event.getAuthNotesFragment());
+    }
+
+    private static void updateAuthSession(AuthenticationSessionEntity authSession, Map<String, String> authNotesFragment) {
+        if (authSession != null) {
+            if (authSession.getAuthNotes() == null) {
+                authSession.setAuthNotes(new ConcurrentHashMap<>());
+            }
+
+            for (Entry<String, String> me : authNotesFragment.entrySet()) {
+                String value = me.getValue();
+                if (value == null) {
+                    authSession.getAuthNotes().remove(me.getKey());
+                } else {
+                    authSession.getAuthNotes().put(me.getKey(), value);
+                }
+            }
+        }
+    }
+
+    private void lazyInit(KeycloakSession session) {
+        if (authSessionsCache == null) {
+            synchronized (this) {
+                if (authSessionsCache == null) {
+                    InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
+                    authSessionsCache = connections.getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME);
+
+                    ClusterProvider cluster = session.getProvider(ClusterProvider.class);
+                    cluster.registerListener(AUTHENTICATION_SESSION_EVENTS, this::updateAuthNotes);
+
+                    log.debug("Registered cluster listeners");
+                }
+            }
+        }
+    }
+
     @Override
     public void postInit(KeycloakSessionFactory factory) {
     }
diff --git a/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java
index f284d93..99806d4 100644
--- a/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java
+++ b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java
@@ -20,6 +20,7 @@ package org.keycloak.sessions;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.provider.Provider;
+import java.util.Map;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -39,5 +40,14 @@ public interface AuthenticationSessionProvider extends Provider {
     void onRealmRemoved(RealmModel realm);
     void onClientRemoved(RealmModel realm, ClientModel client);
 
+    /**
+     * Requests update of authNotes of an authentication session that is not owned
+     * by this instance but might exist somewhere in the cluster.
+     * 
+     * @param authSessionId
+     * @param authNotesFragment Map with authNote values. Auth note is removed if the corresponding value in the map is {@code null}.
+     */
+    void updateNonlocalSessionAuthNotes(String authSessionId, Map<String, String> authNotesFragment);
+
 
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/events/EventType.java b/server-spi-private/src/main/java/org/keycloak/events/EventType.java
index 583b0d4..920646f 100755
--- a/server-spi-private/src/main/java/org/keycloak/events/EventType.java
+++ b/server-spi-private/src/main/java/org/keycloak/events/EventType.java
@@ -92,6 +92,8 @@ public enum EventType {
     USER_INFO_REQUEST(false),
     USER_INFO_REQUEST_ERROR(false),
 
+    IDENTITY_PROVIDER_LINK_ACCOUNT(true),
+    IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR(true),
     IDENTITY_PROVIDER_LOGIN(false),
     IDENTITY_PROVIDER_LOGIN_ERROR(false),
     IDENTITY_PROVIDER_FIRST_LOGIN(true),
@@ -129,6 +131,10 @@ public enum EventType {
         this.saveByDefault = saveByDefault;
     }
 
+    /**
+     * Determines whether this event is stored when the admin has not set a specific set of event types to save.
+     * @return
+     */
     public boolean isSaveByDefault() {
         return saveByDefault;
     }
diff --git a/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionProviderFactory.java
index b182458..c8758ca 100644
--- a/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionProviderFactory.java
+++ b/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionProviderFactory.java
@@ -23,4 +23,6 @@ import org.keycloak.provider.ProviderFactory;
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
 public interface AuthenticationSessionProviderFactory extends ProviderFactory<AuthenticationSessionProvider> {
+    // TODO:hmlnarik: move this constant out of an interface into a more appropriate class
+    public static final String AUTHENTICATION_SESSION_EVENTS = "AUTHENTICATION_SESSION_EVENTS";
 }
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java
index 26598c1..ce45deb 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java
@@ -17,6 +17,7 @@
 package org.keycloak.authentication.actiontoken;
 
 import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.AuthenticationProcessor;
 import org.keycloak.common.ClientConnection;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.models.*;
@@ -25,6 +26,8 @@ import org.keycloak.representations.JsonWebToken;
 import org.keycloak.services.Urls;
 import org.keycloak.services.managers.AuthenticationSessionManager;
 import org.keycloak.sessions.AuthenticationSessionModel;
+import java.util.function.Function;
+import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriBuilderException;
 import javax.ws.rs.core.UriInfo;
 import org.jboss.resteasy.spi.HttpRequest;
@@ -35,6 +38,16 @@ import org.jboss.resteasy.spi.HttpRequest;
  */
 public class ActionTokenContext<T extends JsonWebToken> {
 
+    @FunctionalInterface
+    public interface ProcessAuthenticateFlow {
+        Response processFlow(boolean action, String execution, AuthenticationSessionModel authSession, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor);
+    };
+
+    @FunctionalInterface
+    public interface ProcessBrokerFlow {
+        Response brokerLoginFlow(String code, String execution, String flowPath);
+    };
+
     private final KeycloakSession session;
     private final RealmModel realm;
     private final UriInfo uriInfo;
@@ -45,8 +58,13 @@ public class ActionTokenContext<T extends JsonWebToken> {
     private AuthenticationSessionModel authenticationSession;
     private boolean authenticationSessionFresh;
     private String executionId;
+    private final ProcessAuthenticateFlow processAuthenticateFlow;
+    private final ProcessBrokerFlow processBrokerFlow;
 
-    public ActionTokenContext(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection, HttpRequest request, EventBuilder event, ActionTokenHandler<T> handler) {
+    public ActionTokenContext(KeycloakSession session, RealmModel realm, UriInfo uriInfo,
+      ClientConnection clientConnection, HttpRequest request,
+      EventBuilder event, ActionTokenHandler<T> handler, String executionId,
+      ProcessAuthenticateFlow processFlow, ProcessBrokerFlow processBrokerFlow) {
         this.session = session;
         this.realm = realm;
         this.uriInfo = uriInfo;
@@ -54,6 +72,9 @@ public class ActionTokenContext<T extends JsonWebToken> {
         this.request = request;
         this.event = event;
         this.handler = handler;
+        this.executionId = executionId;
+        this.processAuthenticateFlow = processFlow;
+        this.processBrokerFlow = processBrokerFlow;
     }
 
     public EventBuilder getEvent() {
@@ -131,4 +152,12 @@ public class ActionTokenContext<T extends JsonWebToken> {
     public void setExecutionId(String executionId) {
         this.executionId = executionId;
     }
+
+    public Response processFlow(boolean action, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor) {
+        return processAuthenticateFlow.processFlow(action, getExecutionId(), getAuthenticationSession(), flowPath, flow, errorMessage, processor);
+    }
+
+    public Response brokerFlow(String code, String flowPath) {
+        return processBrokerFlow.brokerLoginFlow(code, getExecutionId(), flowPath);
+    }
 }
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java
index e573df4..4368a74 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java
@@ -35,21 +35,6 @@ import javax.ws.rs.core.Response;
  */
 public interface ActionTokenHandler<T extends JsonWebToken> extends Provider {
 
-    @FunctionalInterface
-    public interface ProcessFlow {
-        Response processFlow(boolean action, String execution, AuthenticationSessionModel authSession, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor);
-    };
-
-    /**
-     * Returns an array of verifiers that are tested prior to handling the token. All verifiers have to pass successfully
-     * for token to be handled. The returned array must not be {@code null}.
-     * @param tokenContext
-     * @return Verifiers or an empty array
-     */
-    default Predicate<? super T>[] getVerifiers(ActionTokenContext<T> tokenContext) {
-        return new Predicate[] {};
-    }
-
     /**
      * Performs the action as per the token details. This method is only called if all verifiers
      * returned in {@link #handleToken} succeed.
@@ -59,7 +44,7 @@ public interface ActionTokenHandler<T extends JsonWebToken> extends Provider {
      * @return
      * @throws VerificationException
      */
-    Response handleToken(T token, ActionTokenContext<T> tokenContext, ProcessFlow processFlow);
+    Response handleToken(T token, ActionTokenContext<T> tokenContext);
 
     /**
      * Returns the Java token class for use with deserialization.
@@ -68,6 +53,16 @@ public interface ActionTokenHandler<T extends JsonWebToken> extends Provider {
     Class<T> getTokenClass();
 
     /**
+     * Returns an array of verifiers that are tested prior to handling the token. All verifiers have to pass successfully
+     * for token to be handled. The returned array must not be {@code null}.
+     * @param tokenContext
+     * @return Verifiers or an empty array. The returned array must not be {@code null}.
+     */
+    default Predicate<? super T>[] getVerifiers(ActionTokenContext<T> tokenContext) {
+        return new Predicate[] {};
+    }
+
+    /**
      * Returns an authentication session ID requested from within the given token
      * @param token Token. Can be {@code null}
      * @return authentication session ID
@@ -95,17 +90,8 @@ public interface ActionTokenHandler<T extends JsonWebToken> extends Provider {
     String getDefaultErrorMessage();
 
     /**
-     * Returns a response that restarts a flow that this action token initiates, or {@code null} if
-     * no special handling is requested.
-     * 
-     * @return
-     */
-    default Response handleRestartRequest(T token, ActionTokenContext<T> tokenContext, ProcessFlow processFlow) {
-        return null;
-    }
-
-    /**
-     * Creates a fresh authentication session according to the information from the token.
+     * Creates a fresh authentication session according to the information from the token. The default
+     * implementation creates a new authentication session that requests termination after required actions.
      * @param token
      * @param tokenContext
      * @return
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java
index 117c465..a5440a9 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java
@@ -25,7 +25,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
  */
 public class DefaultActionTokenKey extends JsonWebToken {
 
-    // The authenticationSession note with ID of the user authenticated via the action token
+    /** The authenticationSession note with ID of the user authenticated via the action token */
     public static final String ACTION_TOKEN_USER_ID = "ACTION_TOKEN_USER";
 
     public DefaultActionTokenKey(String userId, String actionId) {
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 691fff5..010c517 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
@@ -25,9 +25,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.protocol.oidc.utils.RedirectUtils;
 import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.messages.Messages;
-import org.keycloak.services.resources.LoginActionsServiceChecks;
 import org.keycloak.sessions.AuthenticationSessionModel;
-import org.keycloak.sessions.CommonClientSessionModel.Action;
 import javax.ws.rs.core.Response;
 
 /**
@@ -61,7 +59,7 @@ public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHander<
     }
 
     @Override
-    public Response handleToken(ExecuteActionsActionToken token, ActionTokenContext<ExecuteActionsActionToken> tokenContext, ProcessFlow processFlow) {
+    public Response handleToken(ExecuteActionsActionToken token, ActionTokenContext<ExecuteActionsActionToken> tokenContext) {
         AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
 
         String redirectUri = RedirectUtils.verifyRedirectUri(tokenContext.getUriInfo(), token.getRedirectUri(),
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
new file mode 100644
index 0000000..ea705ed
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java
@@ -0,0 +1,69 @@
+/*
+ * 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.authentication.actiontoken.idpverifyemail;
+
+import org.keycloak.authentication.actiontoken.verifyemail.*;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.UUID;
+import org.keycloak.authentication.actiontoken.DefaultActionToken;
+
+/**
+ * Representation of a token that represents a time-limited verify e-mail action.
+ *
+ * @author hmlnarik
+ */
+public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
+
+    public static final String TOKEN_TYPE = "idp-verify-account-via-email";
+
+    private static final String JSON_FIELD_IDENTITY_PROVIDER_USERNAME = "idpu";
+    private static final String JSON_FIELD_IDENTITY_PROVIDER_ALIAS = "idpa";
+
+    @JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_USERNAME)
+    private String identityProviderUsername;
+
+    @JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_ALIAS)
+    private String identityProviderAlias;
+
+    public IdpVerifyAccountLinkActionToken(String userId, int absoluteExpirationInSecs, UUID actionVerificationNonce, String authenticationSessionId,
+      String identityProviderUsername, String identityProviderAlias) {
+        super(userId, TOKEN_TYPE, absoluteExpirationInSecs, actionVerificationNonce);
+        setAuthenticationSessionId(authenticationSessionId);
+        this.identityProviderUsername = identityProviderUsername;
+        this.identityProviderAlias = identityProviderAlias;
+    }
+
+    private IdpVerifyAccountLinkActionToken() {
+        super(null, TOKEN_TYPE, -1, null);
+    }
+
+    public String getIdentityProviderUsername() {
+        return identityProviderUsername;
+    }
+
+    public void setIdentityProviderUsername(String identityProviderUsername) {
+        this.identityProviderUsername = identityProviderUsername;
+    }
+
+    public String getIdentityProviderAlias() {
+        return identityProviderAlias;
+    }
+
+    public void setIdentityProviderAlias(String identityProviderAlias) {
+        this.identityProviderAlias = identityProviderAlias;
+    }
+}
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
new file mode 100644
index 0000000..3aec118
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java
@@ -0,0 +1,97 @@
+/*
+ * 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.authentication.actiontoken.idpverifyemail;
+
+import org.keycloak.authentication.actiontoken.AbstractActionTokenHander;
+import org.keycloak.TokenVerifier.Predicate;
+import org.keycloak.authentication.AuthenticationProcessor;
+import org.keycloak.authentication.actiontoken.*;
+import org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticator;
+import org.keycloak.events.*;
+import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.models.UserModel;
+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;
+
+/**
+ * Action token handler for verification of e-mail address.
+ * @author hmlnarik
+ */
+public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenHander<IdpVerifyAccountLinkActionToken> {
+
+    public IdpVerifyAccountLinkActionTokenHandler() {
+        super(
+          IdpVerifyAccountLinkActionToken.TOKEN_TYPE,
+          IdpVerifyAccountLinkActionToken.class,
+          Messages.STALE_CODE,
+          EventType.IDENTITY_PROVIDER_LINK_ACCOUNT,
+          Errors.INVALID_TOKEN
+        );
+    }
+
+    @Override
+    public Predicate<? super IdpVerifyAccountLinkActionToken>[] getVerifiers(ActionTokenContext<IdpVerifyAccountLinkActionToken> tokenContext) {
+        return TokenUtils.predicates(
+        );
+    }
+
+    @Override
+    public Response handleToken(IdpVerifyAccountLinkActionToken token, ActionTokenContext<IdpVerifyAccountLinkActionToken> tokenContext) {
+        UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser();
+        EventBuilder event = tokenContext.getEvent();
+
+        event.event(EventType.IDENTITY_PROVIDER_LINK_ACCOUNT)
+          .detail(Details.EMAIL, user.getEmail())
+          .detail(Details.IDENTITY_PROVIDER, token.getIdentityProviderAlias())
+          .detail(Details.IDENTITY_PROVIDER_USERNAME, token.getIdentityProviderUsername())
+          .success();
+
+        // 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);
+
+            AuthenticationSessionProvider authSessProvider = tokenContext.getSession().authenticationSessions();
+            authSession = authSessProvider.getAuthenticationSession(tokenContext.getRealm(), token.getAuthenticationSessionId());
+
+            if (authSession != null) {
+                authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername());
+            } else {
+                authSessProvider.updateNonlocalSessionAuthNotes(
+                  token.getAuthenticationSessionId(),
+                  Collections.singletonMap(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername())
+                );
+            }
+
+            return tokenContext.getSession().getProvider(LoginFormsProvider.class)
+                    .setSuccess(Messages.IDENTITY_PROVIDER_LINK_SUCCESS, token.getIdentityProviderAlias(), token.getIdentityProviderUsername())
+                    .createInfoPage();
+        }
+
+        authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername());
+
+        return tokenContext.brokerFlow(null, authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH));
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionTokenHandler.java
index c6f834b..3417431 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionTokenHandler.java
@@ -62,13 +62,11 @@ public class ResetCredentialsActionTokenHandler extends AbstractActionTokenHande
     }
 
     @Override
-    public Response handleToken(ResetCredentialsActionToken token, ActionTokenContext tokenContext, ProcessFlow processFlow) {
+    public Response handleToken(ResetCredentialsActionToken token, ActionTokenContext tokenContext) {
         AuthenticationProcessor authProcessor = new ResetCredsAuthenticationProcessor();
 
-        return processFlow.processFlow(
+        return tokenContext.processFlow(
           false,
-          tokenContext.getExecutionId(),
-          tokenContext.getAuthenticationSession(),
           RESET_CREDENTIALS_PATH,
           tokenContext.getRealm().getResetCredentialsFlow(),
           null,
@@ -76,17 +74,6 @@ public class ResetCredentialsActionTokenHandler extends AbstractActionTokenHande
         );
     }
 
-    @Override
-    public Response handleRestartRequest(ResetCredentialsActionToken token, ActionTokenContext<ResetCredentialsActionToken> tokenContext, ProcessFlow processFlow) {
-        // In the case restart is requested, the handling is exactly the same as if a token had been
-        // handled correctly but with a fresh authentication session
-        AuthenticationSessionManager asm = new AuthenticationSessionManager(tokenContext.getSession());
-        asm.removeAuthenticationSession(tokenContext.getRealm(), tokenContext.getAuthenticationSession(), false);
-
-        tokenContext.setAuthenticationSession(tokenContext.createAuthenticationSessionForClient(null), true);
-        return handleToken(token, tokenContext, processFlow);
-    }
-
     public static class ResetCredsAuthenticationProcessor extends AuthenticationProcessor {
 
         @Override
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 1d324c2..abe2127 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
@@ -57,7 +57,7 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHander<Ver
     }
 
     @Override
-    public Response handleToken(VerifyEmailActionToken token, ActionTokenContext<VerifyEmailActionToken> tokenContext, ProcessFlow processFlow) {
+        public Response handleToken(VerifyEmailActionToken token, ActionTokenContext<VerifyEmailActionToken> tokenContext) {
         UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser();
         EventBuilder event = tokenContext.getEvent();
 
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
index b70f69b..d8b9b30 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
@@ -20,9 +20,10 @@ package org.keycloak.authentication.authenticators.broker;
 import org.jboss.logging.Logger;
 import org.keycloak.authentication.AuthenticationFlowContext;
 import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.authentication.actiontoken.idpverifyemail.IdpVerifyAccountLinkActionToken;
 import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
-import org.keycloak.authentication.requiredactions.VerifyEmail;
 import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.common.util.Time;
 import org.keycloak.email.EmailException;
 import org.keycloak.email.EmailTemplateProvider;
 import org.keycloak.events.Details;
@@ -30,20 +31,21 @@ import org.keycloak.events.Errors;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.events.EventType;
 import org.keycloak.forms.login.LoginFormsProvider;
-import org.keycloak.models.ClientSessionModel;
-import org.keycloak.models.Constants;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.services.ServicesLogger;
+import org.keycloak.services.Urls;
+import org.keycloak.services.managers.ClientSessionCode;
 import org.keycloak.services.messages.Messages;
-import org.keycloak.services.resources.LoginActionsService;
 import org.keycloak.sessions.AuthenticationSessionModel;
 
-import javax.ws.rs.core.MultivaluedMap;
+import java.net.URI;
+import java.util.Objects;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriBuilder;
 import java.util.concurrent.TimeUnit;
+import javax.ws.rs.core.*;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -52,42 +54,85 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator 
 
     private static Logger logger = Logger.getLogger(IdpEmailVerificationAuthenticator.class);
 
+    public static final String VERIFY_ACCOUNT_IDP_USERNAME = "VERIFY_ACCOUNT_IDP_USERNAME";
+
     @Override
     protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
         KeycloakSession session = context.getSession();
         RealmModel realm = context.getRealm();
         AuthenticationSessionModel authSession = context.getAuthenticationSession();
 
-        if (realm.getSmtpConfig().size() == 0) {
+        if (realm.getSmtpConfig().isEmpty()) {
             ServicesLogger.LOGGER.smtpNotConfigured();
             context.attempted();
             return;
         }
-/*
-        VerifyEmail.setupKey(clientSession);
 
-        UserModel existingUser = getExistingUser(session, realm, clientSession);
+        if (Objects.equals(authSession.getAuthNote(VERIFY_ACCOUNT_IDP_USERNAME), brokerContext.getUsername())) {
+            UserModel existingUser = getExistingUser(session, realm, authSession);
+
+            logger.debugf("User '%s' confirmed that wants to link with identity provider '%s' . Identity provider username is '%s' ", existingUser.getUsername(),
+                    brokerContext.getIdpConfig().getAlias(), brokerContext.getUsername());
+
+            context.setUser(existingUser);
+            context.success();
+            return;
+        }
+
+        UserModel existingUser = getExistingUser(session, realm, authSession);
+
+        sendVerifyEmail(session, context, existingUser, brokerContext);
+    }
+
+    @Override
+    protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
+        logger.debugf("Re-sending email requested for user, details follow");
+
+        // This will allow user to re-send email again
+        context.getAuthenticationSession().removeAuthNote(VERIFY_ACCOUNT_IDP_USERNAME);
+        authenticateImpl(context, serializedCtx, brokerContext);
+    }
+
+    @Override
+    public boolean requiresUser() {
+        return false;
+    }
 
-        String link = UriBuilder.fromUri(context.getActionUrl())
-                .queryParam(Constants.KEY, clientSession.getNote(Constants.VERIFY_EMAIL_KEY))
-                .build().toString();
+    @Override
+    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
+        return false;
+    }
+
+    private void sendVerifyEmail(KeycloakSession session, AuthenticationFlowContext context, UserModel existingUser, BrokeredIdentityContext brokerContext) throws UriBuilderException, IllegalArgumentException {
+        RealmModel realm = session.getContext().getRealm();
+        UriInfo uriInfo = session.getContext().getUri();
+        AuthenticationSessionModel authSession = context.getAuthenticationSession();
+
+        int validityInSecs = realm.getAccessCodeLifespanUserAction();
+        int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
 
         EventBuilder event = context.getEvent().clone().event(EventType.SEND_IDENTITY_PROVIDER_LINK)
                 .user(existingUser)
                 .detail(Details.USERNAME, existingUser.getUsername())
                 .detail(Details.EMAIL, existingUser.getEmail())
-                .detail(Details.CODE_ID, clientSession.getId())
+                .detail(Details.CODE_ID, authSession.getId())
                 .removeDetail(Details.AUTH_METHOD)
                 .removeDetail(Details.AUTH_TYPE);
 
-        long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction());
-        try {
+        IdpVerifyAccountLinkActionToken token = new IdpVerifyAccountLinkActionToken(
+          existingUser.getId(), absoluteExpirationInSecs, null, authSession.getId(),
+          brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias()
+        );
+        UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
+        String link = builder.queryParam("execution", context.getExecution().getId()).build(realm.getName()).toString();
+        long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
 
+        try {
             context.getSession().getProvider(EmailTemplateProvider.class)
                     .setRealm(realm)
                     .setUser(existingUser)
                     .setAttribute(EmailTemplateProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext)
-                    .sendConfirmIdentityBrokerLink(link, expiration);
+                    .sendConfirmIdentityBrokerLink(link, expirationInMinutes);
 
             event.success();
         } catch (EmailException e) {
@@ -101,62 +146,14 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator 
             return;
         }
 
+        String accessCode = context.generateAccessCode();
+        URI action = context.getActionUrl(accessCode);
+
         Response challenge = context.form()
                 .setStatus(Response.Status.OK)
                 .setAttribute(LoginFormsProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext)
+                .setActionUri(action)
                 .createIdpLinkEmailPage();
-        context.forceChallenge(challenge);*/
-    }
-
-    @Override
-    protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
-        /*MultivaluedMap<String, String> queryParams = context.getSession().getContext().getUri().getQueryParameters();
-        String key = queryParams.getFirst(Constants.KEY);
-        ClientSessionModel clientSession = context.getClientSession();
-        RealmModel realm = context.getRealm();
-        KeycloakSession session = context.getSession();
-
-        if (key != null) {
-            String keyFromSession = clientSession.getNote(Constants.VERIFY_EMAIL_KEY);
-            clientSession.removeNote(Constants.VERIFY_EMAIL_KEY);
-            if (key.equals(keyFromSession)) {
-                UserModel existingUser = getExistingUser(session, realm, clientSession);
-
-                logger.debugf("User '%s' confirmed that wants to link with identity provider '%s' . Identity provider username is '%s' ", existingUser.getUsername(),
-                        brokerContext.getIdpConfig().getAlias(), brokerContext.getUsername());
-
-                String actionCookieValue = LoginActionsService.getActionCookie(session.getContext().getRequestHeaders(), realm, session.getContext().getUri(), context.getConnection());
-                if (actionCookieValue == null || !actionCookieValue.equals(clientSession.getId())) {
-                    clientSession.setNote(IS_DIFFERENT_BROWSER, "true");
-                }
-
-                // User successfully confirmed linking by email verification. His email was defacto verified
-                existingUser.setEmailVerified(true);
-
-                context.setUser(existingUser);
-                context.success();
-            } else {
-                ServicesLogger.LOGGER.keyParamDoesNotMatch();
-                Response challengeResponse = context.form()
-                        .setError(Messages.INVALID_ACCESS_CODE)
-                        .createErrorPage();
-                context.failureChallenge(AuthenticationFlowError.IDENTITY_PROVIDER_ERROR, challengeResponse);
-            }
-        } else {
-            Response challengeResponse = context.form()
-                    .setError(Messages.MISSING_PARAMETER, Constants.KEY)
-                    .createErrorPage();
-            context.failureChallenge(AuthenticationFlowError.IDENTITY_PROVIDER_ERROR, challengeResponse);
-        }*/
-    }
-
-    @Override
-    public boolean requiresUser() {
-        return false;
-    }
-
-    @Override
-    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
-        return false;
+        context.forceChallenge(challenge);
     }
 }
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
index f3ea22f..ad84f9d 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
@@ -31,7 +31,6 @@ import org.keycloak.events.EventType;
 import org.keycloak.forms.login.LoginFormsProvider;
 import org.keycloak.models.*;
 import org.keycloak.models.UserModel.RequiredAction;
-import org.keycloak.models.utils.HmacOTP;
 import org.keycloak.services.Urls;
 import org.keycloak.services.validation.Validation;
 
@@ -87,7 +86,7 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
 
     @Override
     public void processAction(RequiredActionContext context) {
-        logger.infof("Re-sending email requested for user: %s", context.getUser().getUsername());
+        logger.debugf("Re-sending email requested for user: %s", context.getUser().getUsername());
 
         // This will allow user to re-send email again
         context.getAuthenticationSession().removeAuthNote(Constants.VERIFY_EMAIL_KEY);
@@ -152,9 +151,4 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
 
         return forms.createResponse(UserModel.RequiredAction.VERIFY_EMAIL);
     }
-
-    public static void setupKey(AuthenticationSessionModel session) {
-        String secret = HmacOTP.generateSecret(10);
-        session.setAuthNote(Constants.VERIFY_EMAIL_KEY, secret);
-    }
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
index 5dcb621..758b7a1 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -67,7 +67,6 @@ import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.managers.AuthenticationSessionManager;
 import org.keycloak.services.managers.ClientSessionCode;
 import org.keycloak.services.messages.Messages;
-import org.keycloak.services.resources.LoginActionsServiceChecks.RestartFlowException;
 import org.keycloak.services.util.CacheControlUtil;
 import org.keycloak.services.util.AuthenticationFlowURLHelper;
 import org.keycloak.services.util.BrowserHistoryHelper;
@@ -448,7 +447,6 @@ public class LoginActionsService {
               .secretKey(session.keys().getActiveHmacKey(realm).getSecretKey())
               .verify();
 
-            // TODO:hmlnarik Optimize
             token = TokenVerifier.create(tokenString, handler.getTokenClass()).getToken();
         } catch (TokenNotActiveException ex) {
             if (authSession != null) {
@@ -469,40 +467,32 @@ public class LoginActionsService {
         }
 
         // Now proceed with the verification and handle the token
-        tokenContext = new ActionTokenContext(session, realm, uriInfo, clientConnection, request, event, handler);
+        tokenContext = new ActionTokenContext(session, realm, uriInfo, clientConnection, request, event, handler, execution, this::processFlow, this::brokerLoginFlow);
 
         try {
-            tokenContext.setExecutionId(execution);
-
             String tokenAuthSessionId = handler.getAuthenticationSessionIdFromToken(token);
-            if (authSession == null) {
-                if (tokenAuthSessionId != null) {
-                    // This can happen if the token contains ID but user opens the link in a new browser
-                    LoginActionsServiceChecks.checkNotLoggedInYet(tokenContext, tokenAuthSessionId);
-                }
 
+            if (tokenAuthSessionId != null) {
+                // This can happen if the token contains ID but user opens the link in a new browser
+                LoginActionsServiceChecks.checkNotLoggedInYet(tokenContext, tokenAuthSessionId);
+            }
+
+            if (authSession == null) {
                 authSession = handler.startFreshAuthenticationSession(token, tokenContext);
                 tokenContext.setAuthenticationSession(authSession, true);
+            } else if (tokenAuthSessionId == null ||
+              ! LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, tokenAuthSessionId)) {
+                // There exists an authentication session but no auth session ID was received in the action token
+                logger.debugf("Authentication session in progress but no authentication session ID was found in action token %s, restarting.", token.getId());
+                new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, false);
 
-                initLoginEvent(authSession);
-                event.event(handler.eventType());
-            } else {
-                initLoginEvent(authSession);
-                event.event(handler.eventType());
-
-                if (tokenAuthSessionId == null) {
-                    // There exists an authentication session but no auth session ID was received in the action token
-                    logger.debugf("Authentication session exists while reauthentication was requested by using action token %s, restarting.", token.getId());
-                    new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, false);
-
-                    authSession = handler.startFreshAuthenticationSession(token, tokenContext);
-                    tokenContext.setAuthenticationSession(authSession, true);
-                } else {
-                    LoginActionsServiceChecks.checkNotLoggedInYet(tokenContext, tokenAuthSessionId);
-                    LoginActionsServiceChecks.checkAuthenticationSessionFromCookieMatchesOneFromToken(tokenContext, tokenAuthSessionId);
-                }
+                authSession = handler.startFreshAuthenticationSession(token, tokenContext);
+                tokenContext.setAuthenticationSession(authSession, true);
             }
 
+            initLoginEvent(authSession);
+            event.event(handler.eventType());
+
             LoginActionsServiceChecks.checkIsUserValid(token, tokenContext);
             LoginActionsServiceChecks.checkIsClientValid(token, tokenContext);
             
@@ -519,14 +509,9 @@ public class LoginActionsService {
 
             authSession.setAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID, token.getUserId());
 
-            return handler.handleToken(token, tokenContext, this::processFlow);
+            return handler.handleToken(token, tokenContext);
         } catch (ExplainedTokenVerificationException ex) {
             return handleActionTokenVerificationException(tokenContext, ex, ex.getErrorEvent(), ex.getMessage());
-        } catch (RestartFlowException ex) {
-            Response response = handler.handleRestartRequest(token, tokenContext, this::processFlow);
-            return response == null
-              ? handleActionTokenVerificationException(tokenContext, ex, eventError, defaultErrorMessage)
-              : response;
         } catch (LoginActionsServiceException ex) {
             Response response = ex.getResponse();
             return response == null
@@ -779,37 +764,6 @@ public class LoginActionsService {
         return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, clientSession.getUserSession(), clientSession, request, uriInfo, clientConnection, event, authSession.getProtocol());
     }
 
-
-    /**
-     * Initiated by admin, not the user on login
-     *
-     * @param key
-     * @return
-     */
-    @Path("execute-actions")
-    @GET
-    public Response executeActions(@QueryParam("key") String key) {
-        // TODO:mposolda
-        /*
-        event.event(EventType.EXECUTE_ACTIONS);
-        if (key != null) {
-            SessionCodeChecks checks = checksForCode(key);
-            if (!checks.verifyCode(ClientSessionModel.Action.EXECUTE_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
-                return checks.response;
-            }
-            ClientSessionModel clientSession = checks.getClientSession();
-            // verify user email as we know it is valid as this entry point would never have gotten here.
-            clientSession.getUserSession().getUser().setEmailVerified(true);
-            clientSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
-            clientSession.setNote(ClientSessionModel.Action.EXECUTE_ACTIONS.name(), "true");
-            return AuthenticationProcessor.redirectToRequiredActions(session, realm, clientSession, uriInfo);
-        } else {
-            event.error(Errors.INVALID_CODE);
-            return ErrorPage.error(session, Messages.INVALID_CODE);
-        }*/
-        return null;
-    }
-
     private void initLoginEvent(AuthenticationSessionModel authSession) {
         String responseType = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
         if (responseType == null) {
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java
index cabb1b6..6d42d25 100644
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java
@@ -45,11 +45,6 @@ public class LoginActionsServiceChecks {
     private static final Logger LOG = Logger.getLogger(LoginActionsServiceChecks.class.getName());
 
     /**
-     * Exception signalling that flow needs to be restarted because authentication session IDs from cookie and token do not match.
-     */
-    public static class RestartFlowException extends VerificationException { }
-
-    /**
      * This check verifies that user ID (subject) from the token matches
      * the one from the authentication session.
      */
@@ -264,32 +259,32 @@ public class LoginActionsServiceChecks {
      *
      *  @param <T>
      */
-    public static <T extends JsonWebToken> void checkAuthenticationSessionFromCookieMatchesOneFromToken(ActionTokenContext<T> context, String authSessionIdFromToken) throws VerificationException {
+    public static <T extends JsonWebToken> boolean doesAuthenticationSessionFromCookieMatchOneFromToken(ActionTokenContext<T> context, String authSessionIdFromToken) throws VerificationException {
         if (authSessionIdFromToken == null) {
-            throw new RestartFlowException();
+            return false;
         }
 
         AuthenticationSessionManager asm = new AuthenticationSessionManager(context.getSession());
         String authSessionIdFromCookie = asm.getCurrentAuthenticationSessionId(context.getRealm());
 
         if (authSessionIdFromCookie == null) {
-            throw new RestartFlowException();
+            return false;
         }
 
         AuthenticationSessionModel authSessionFromCookie = context.getSession()
           .authenticationSessions().getAuthenticationSession(context.getRealm(), authSessionIdFromCookie);
         if (authSessionFromCookie == null) {    // Cookie contains ID of expired auth session
-            throw new RestartFlowException();
+            return false;
         }
 
         if (Objects.equals(authSessionIdFromCookie, authSessionIdFromToken)) {
             context.setAuthenticationSession(authSessionFromCookie, false);
-            return;
+            return true;
         }
 
         String parentSessionId = authSessionFromCookie.getAuthNote(AuthenticationProcessor.FORKED_FROM);
         if (parentSessionId == null || ! Objects.equals(authSessionIdFromToken, parentSessionId)) {
-            throw new RestartFlowException();
+            return false;
         }
 
         AuthenticationSessionModel authSessionFromParent = context.getSession()
@@ -299,12 +294,14 @@ public class LoginActionsServiceChecks {
         // from the login form (browser flow) but from the token's flow
         // Don't expire KC_RESTART cookie at this point
         asm.removeAuthenticationSession(context.getRealm(), authSessionFromCookie, false);
-        LOG.infof("Removed forked session: %s", authSessionFromCookie.getId());
+        LOG.debugf("Removed forked session: %s", authSessionFromCookie.getId());
 
         // Refresh browser cookie
         asm.setAuthSessionCookie(parentSessionId, context.getRealm());
 
         context.setAuthenticationSession(authSessionFromParent, false);
         context.setExecutionId(authSessionFromParent.getAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION));
+
+        return true;
     }
 }
diff --git a/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java
index edeac76..e92aa05 100755
--- a/services/src/main/java/org/keycloak/services/Urls.java
+++ b/services/src/main/java/org/keycloak/services/Urls.java
@@ -178,10 +178,6 @@ public class Urls {
         return loginResetCredentialsBuilder(baseUri).build(realmName);
     }
 
-    public static UriBuilder executeActionsBuilder(URI baseUri) {
-        return loginActionsBase(baseUri).path(LoginActionsService.class, "executeActions");
-    }
-
     public static UriBuilder actionTokenBuilder(URI baseUri, String tokenString) {
         return loginActionsBase(baseUri).path(LoginActionsService.class, "executeActionToken")
           .queryParam("key", tokenString);
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory
index 246758d..2a5b9ec 100644
--- a/services/src/main/resources/META-INF/services/org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory
@@ -1,3 +1,4 @@
 org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionTokenHandler
 org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionTokenHandler
 org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionTokenHandler
+org.keycloak.authentication.actiontoken.idpverifyemail.IdpVerifyAccountLinkActionTokenHandler
\ No newline at end of file
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 f2dffaf..ba7bb65 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
@@ -52,6 +52,8 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.startsWith;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
@@ -298,6 +300,147 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi
         Assert.assertTrue(user.isEmailVerified());
     }
 
+    /**
+     * Tests that duplication is detected and user wants to link federatedIdentity with existing account. He will confirm link by email
+     */
+    @Test
+    public void testLinkAccountByEmailVerificationTwice() throws Exception {
+        setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF);
+
+        loginIDP("pedroigor");
+
+        this.idpConfirmLinkPage.assertCurrent();
+        Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage());
+        this.idpConfirmLinkPage.clickLinkAccount();
+
+        // Confirm linking account by email
+        this.idpLinkEmailPage.assertCurrent();
+        Assert.assertThat(
+          this.idpLinkEmailPage.getMessage(),
+          is("An email with instructions to link " + ObjectUtil.capitalize(getProviderId()) + " account pedroigor with your " + APP_REALM_ID + " account has been sent to you.")
+        );
+
+        Assert.assertEquals(1, greenMail.getReceivedMessages().length);
+        MimeMessage message = greenMail.getReceivedMessages()[0];
+        String linkFromMail = getVerificationEmailLink(message);
+
+        driver.navigate().to(linkFromMail.trim());
+
+        // authenticated and redirected to app. User is linked with identity provider
+        assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor");
+
+        // Assert user's email is verified now
+        UserModel user = getFederatedUser();
+        Assert.assertTrue(user.isEmailVerified());
+
+        // Attempt to use the link for the second time
+        driver.navigate().to(linkFromMail.trim());
+
+        infoPage.assertCurrent();
+        Assert.assertThat(infoPage.getInfo(), is("You are already logged in."));
+
+        // Log out
+        driver.navigate().to("http://localhost:8081/test-app/logout");
+
+        // Go to the same link again
+        driver.navigate().to(linkFromMail.trim());
+
+        infoPage.assertCurrent();
+        Assert.assertThat(infoPage.getInfo(), startsWith("Your account was successfully linked with " + getProviderId() + " account pedroigor"));
+    }
+
+    /**
+     * Tests that duplication is detected and user wants to link federatedIdentity with existing account. He will confirm link by email
+     */
+    @Test
+    public void testLinkAccountByEmailVerificationDifferentBrowser() throws Exception, Throwable {
+        setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF);
+
+        loginIDP("pedroigor");
+
+        this.idpConfirmLinkPage.assertCurrent();
+        Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage());
+        this.idpConfirmLinkPage.clickLinkAccount();
+
+        // Confirm linking account by email
+        this.idpLinkEmailPage.assertCurrent();
+        Assert.assertThat(
+          this.idpLinkEmailPage.getMessage(),
+          is("An email with instructions to link " + ObjectUtil.capitalize(getProviderId()) + " account pedroigor with your " + APP_REALM_ID + " account has been sent to you.")
+        );
+
+        Assert.assertEquals(1, greenMail.getReceivedMessages().length);
+        MimeMessage message = greenMail.getReceivedMessages()[0];
+        String linkFromMail = getVerificationEmailLink(message);
+
+        WebRule webRule2 = new WebRule(this);
+        try {
+            webRule2.initProperties();
+
+            WebDriver driver2 = webRule2.getDriver();
+            InfoPage infoPage2 = webRule2.getPage(InfoPage.class);
+
+            driver2.navigate().to(linkFromMail.trim());
+
+            // authenticated, but not redirected to app. Just seeing info page.
+            infoPage2.assertCurrent();
+            Assert.assertThat(infoPage2.getInfo(), startsWith("Your account was successfully linked with " + getProviderId() + " account pedroigor"));
+        } finally {
+            // Revert everything
+            webRule2.after();
+        }
+
+        driver.navigate().refresh();
+        this.loginExpiredPage.assertCurrent();
+        this.loginExpiredPage.clickLoginContinueLink();
+
+        // authenticated and redirected to app. User is linked with identity provider
+        assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor");
+
+        // Assert user's email is verified now
+        UserModel user = getFederatedUser();
+        Assert.assertTrue(user.isEmailVerified());
+    }
+
+    @Test
+    public void testLinkAccountByEmailVerificationResendEmail() throws Exception, Throwable {
+        setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF);
+
+        loginIDP("pedroigor");
+
+        this.idpConfirmLinkPage.assertCurrent();
+        Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage());
+        this.idpConfirmLinkPage.clickLinkAccount();
+
+        // Confirm linking account by email
+        this.idpLinkEmailPage.assertCurrent();
+        Assert.assertThat(
+          this.idpLinkEmailPage.getMessage(),
+          is("An email with instructions to link " + ObjectUtil.capitalize(getProviderId()) + " account pedroigor with your " + APP_REALM_ID + " account has been sent to you.")
+        );
+
+        this.idpLinkEmailPage.clickResendEmail();
+
+        this.idpLinkEmailPage.assertCurrent();
+        Assert.assertThat(
+          this.idpLinkEmailPage.getMessage(),
+          is("An email with instructions to link " + ObjectUtil.capitalize(getProviderId()) + " account pedroigor with your " + APP_REALM_ID + " account has been sent to you.")
+        );
+
+        Assert.assertEquals(2, greenMail.getReceivedMessages().length);
+        MimeMessage message = greenMail.getReceivedMessages()[0];
+        String linkFromMail = getVerificationEmailLink(message);
+
+        driver.navigate().to(linkFromMail.trim());
+
+        // authenticated and redirected to app. User is linked with identity provider
+        assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor");
+
+        // Assert user's email is verified now
+        UserModel user = getFederatedUser();
+        Assert.assertTrue(user.isEmailVerified());
+    }
+
 
     /**
      * Tests that duplication is detected and user wants to link federatedIdentity with existing account. He will confirm link by reauthentication (confirm password on login screen)
@@ -557,29 +700,35 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi
 
         // Simulate 2nd browser
         WebRule webRule2 = new WebRule(this);
-        webRule2.before();
+        try {
+            webRule2.initProperties();
 
-        WebDriver driver2 = webRule2.getDriver();
-        LoginPasswordUpdatePage passwordUpdatePage2 = webRule2.getPage(LoginPasswordUpdatePage.class);
-        InfoPage infoPage2 = webRule2.getPage(InfoPage.class);
+            WebDriver driver2 = webRule2.getDriver();
+            LoginPasswordUpdatePage passwordUpdatePage2 = webRule2.getPage(LoginPasswordUpdatePage.class);
+            InfoPage infoPage2 = webRule2.getPage(InfoPage.class);
 
-        driver2.navigate().to(linkFromMail.trim());
+            driver2.navigate().to(linkFromMail.trim());
 
-        // Need to update password now
-        passwordUpdatePage2.assertCurrent();
-        passwordUpdatePage2.changePassword("password", "password");
+            // Need to update password now
+            passwordUpdatePage2.assertCurrent();
+            passwordUpdatePage2.changePassword("password", "password");
 
-        // authenticated, but not redirected to app. Just seeing info page.
-        infoPage2.assertCurrent();
-        Assert.assertEquals("Your account has been updated.", infoPage2.getInfo());
+            // authenticated, but not redirected to app. Just seeing info page.
+            infoPage2.assertCurrent();
+            Assert.assertEquals("Your account has been updated.", infoPage2.getInfo());
+        } finally {
+            // Revert everything
+            webRule2.after();
+        }
 
         // User is not yet linked with identity provider. He needs to authenticate again in 1st browser
         RealmModel realmWithBroker = getRealm();
         Set<FederatedIdentityModel> federatedIdentities = this.session.users().getFederatedIdentities(this.session.users().getUserByUsername("pedroigor", realmWithBroker), realmWithBroker);
         assertEquals(0, federatedIdentities.size());
 
-        // Continue with 1st browser
-        loginIDP("pedroigor");
+        // Continue with 1st browser. Note that the user has already authenticated with brokered IdP in the beginning of this test
+        // so entering their credentials there is now skipped.
+        loginToIDPWhenAlreadyLoggedIntoProviderIdP("pedroigor");
 
         this.idpConfirmLinkPage.assertCurrent();
         Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage());
@@ -591,9 +740,6 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi
         // authenticated and redirected to app. User is linked with identity provider
         assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor");
 
-        // Revert everything
-        webRule2.after();
-
         brokerServerRule.update(new KeycloakRule.KeycloakSetup() {
 
             @Override
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 8efc8c0..297d00a 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
@@ -37,14 +37,7 @@ import org.keycloak.testsuite.MailUtil;
 import org.keycloak.testsuite.OAuthClient;
 import org.keycloak.testsuite.broker.util.UserSessionStatusServlet;
 import org.keycloak.testsuite.broker.util.UserSessionStatusServlet.UserSessionStatus;
-import org.keycloak.testsuite.pages.AccountFederatedIdentityPage;
-import org.keycloak.testsuite.pages.AccountPasswordPage;
-import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
-import org.keycloak.testsuite.pages.ErrorPage;
-import org.keycloak.testsuite.pages.LoginPage;
-import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
-import org.keycloak.testsuite.pages.OAuthGrantPage;
-import org.keycloak.testsuite.pages.VerifyEmailPage;
+import org.keycloak.testsuite.pages.*;
 import org.keycloak.testsuite.rule.GreenMailRule;
 import org.keycloak.testsuite.rule.LoggingRule;
 import org.keycloak.testsuite.rule.WebResource;
@@ -61,9 +54,8 @@ import java.net.URI;
 import java.util.List;
 import java.util.Set;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.Assert.*;
 
 /**
  * @author pedroigor
@@ -115,6 +107,9 @@ public abstract class AbstractIdentityProviderTest {
     @WebResource
     protected ErrorPage errorPage;
 
+    @WebResource
+    protected InfoPage infoPage;
+
     protected KeycloakSession session;
 
     protected int logoutTimeOffset = 0;
@@ -210,18 +205,29 @@ public abstract class AbstractIdentityProviderTest {
     protected void loginIDP(String username) {
         driver.navigate().to("http://localhost:8081/test-app");
 
-        assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth"));
+        assertThat(this.driver.getCurrentUrl(), startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth"));
 
         // choose the identity provider
         this.loginPage.clickSocial(getProviderId());
 
         String currentUrl = this.driver.getCurrentUrl();
-        assertTrue(currentUrl.startsWith("http://localhost:8082/auth/"));
+        assertThat(currentUrl, startsWith("http://localhost:8082/auth/"));
         // log in to identity provider
         this.loginPage.login(username, "password");
         doAfterProviderAuthentication();
     }
 
+    protected void loginToIDPWhenAlreadyLoggedIntoProviderIdP(String username) {
+        driver.navigate().to("http://localhost:8081/test-app");
+
+        assertThat(this.driver.getCurrentUrl(), startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth"));
+
+        // choose the identity provider
+        this.loginPage.clickSocial(getProviderId());
+
+        doAfterProviderAuthentication();
+    }
+
     protected UserModel getFederatedUser() {
         UserSessionStatus userSessionStatus = retrieveSessionStatus();
         IDToken idToken = userSessionStatus.getIdToken();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java
index 200b0a7..234617b 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java
@@ -23,6 +23,7 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.testsuite.KeycloakServer;
 import org.keycloak.testsuite.rule.AbstractKeycloakRule;
+import org.junit.Test;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpLinkEmailPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpLinkEmailPage.java
index 8ed8461..22eb156 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpLinkEmailPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpLinkEmailPage.java
@@ -28,11 +28,22 @@ public class IdpLinkEmailPage extends AbstractPage {
     @FindBy(id = "instruction1")
     private WebElement message;
 
+    @FindBy(linkText = "Click here")
+    private WebElement resendEmailLink;
+
     @Override
     public boolean isCurrent() {
         return driver.getTitle().startsWith("Link ");
     }
 
+    public void clickResendEmail() {
+        resendEmailLink.click();
+    }
+
+    public String getResendEmailLink() {
+        return resendEmailLink.getAttribute("href");
+    }
+
     @Override
     public void open() throws Exception {
         throw new UnsupportedOperationException();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java
index 2cea40a..0d93d19 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java
@@ -40,10 +40,14 @@ public class WebRule extends ExternalResource {
         this.test = test;
     }
 
-    @Override
-    public void before() throws Throwable {
+    public void initProperties() {
         driver = createWebDriver();
         oauth = new OAuthClient(driver);
+    }
+
+    @Override
+    public void before() throws Throwable {
+        initProperties();
         initWebResources(test);
     }
 
@@ -58,6 +62,7 @@ public class WebRule extends ExternalResource {
             HtmlUnitDriver d = new HtmlUnitDriver();
             d.getWebClient().getOptions().setJavaScriptEnabled(true);
             d.getWebClient().getOptions().setCssEnabled(false);
+            d.getWebClient().getOptions().setTimeout(1000000);
             driver = d;
         } else if (browser.equals("chrome")) {
             driver = new ChromeDriver();
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 7f26d74..f34c320 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
@@ -21,9 +21,7 @@ import org.hamcrest.Matchers;
 import org.jboss.arquillian.drone.api.annotation.Drone;
 import org.jboss.arquillian.graphene.page.Page;
 import org.jboss.arquillian.test.api.ArquillianResource;
-import org.junit.After;
 import org.junit.Assert;
-import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.admin.client.resource.IdentityProviderResource;
@@ -62,7 +60,6 @@ import org.openqa.selenium.WebDriver;
 import javax.mail.MessagingException;
 import javax.mail.internet.MimeMessage;
 import javax.ws.rs.ClientErrorException;
-import javax.ws.rs.NotFoundException;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriBuilder;
 import java.io.IOException;
diff --git a/themes/src/main/resources/theme/base/login/login-idp-link-email.ftl b/themes/src/main/resources/theme/base/login/login-idp-link-email.ftl
index 5dc29f1..9cca544 100644
--- a/themes/src/main/resources/theme/base/login/login-idp-link-email.ftl
+++ b/themes/src/main/resources/theme/base/login/login-idp-link-email.ftl
@@ -9,7 +9,7 @@
             ${msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}
         </p>
         <p id="instruction2" class="instruction">
-            ${msg("emailLinkIdp2")} <a href="${url.firstBrokerLoginUrl}">${msg("doClickHere")}</a> ${msg("emailLinkIdp3")}
+            ${msg("emailLinkIdp2")} <a href="${url.loginAction}">${msg("doClickHere")}</a> ${msg("emailLinkIdp3")}
         </p>
     </#if>
 </@layout.registrationLayout>
\ No newline at end of file