keycloak-aplcache

Changes

Details

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/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/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/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..616f0c6 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
@@ -242,5 +245,12 @@ invalidParameterMessage=Invalid parameter\: {0}
 alreadyLoggedIn=You are already logged in.
 differentUserAuthenticated=You are already authenticated as different user ''{0}'' in this session. Please logout first.
 brokerLinkingSessionExpired=Requested broker account linking, but current session is no longer valid.
+proceedWithAction=&raquo; Click here to proceed
+
+requiredAction.CONFIGURE_TOTP=Configure OTP
+requiredAction.terms_and_conditions=Terms and Conditions
+requiredAction.UPDATE_PASSWORD=Update Password
+requiredAction.UPDATE_PROFILE=Update Profile
+requiredAction.VERIFY_EMAIL=Verify Email
 
 p3pPolicy=CP="This is not a P3P policy!"