keycloak-uncached

Changes

forms/src/main/resources/META-INF/resources/forms/verify-email.ftl 6(+0 -6)

Details

diff --git a/forms/src/main/java/org/keycloak/forms/UrlBean.java b/forms/src/main/java/org/keycloak/forms/UrlBean.java
index b6e2ff3..86b9a6d 100644
--- a/forms/src/main/java/org/keycloak/forms/UrlBean.java
+++ b/forms/src/main/java/org/keycloak/forms/UrlBean.java
@@ -96,6 +96,10 @@ public class UrlBean {
         }
     }
 
+    public String getPasswordResetUrl() {
+        return Urls.accountPasswordReset(baseURI, realm.getId()).toString();
+    }
+
     public String getSocialUrl() {
         return Urls.accountSocialPage(baseURI, realm.getId()).toString();
     }
@@ -104,4 +108,8 @@ public class UrlBean {
         return Urls.accountTotpPage(baseURI, realm.getId()).toString();
     }
 
+    public String getEmailVerificationUrl() {
+        return Urls.accountEmailVerification(baseURI, realm.getId()).toString();
+    }
+
 }
diff --git a/forms/src/main/java/org/keycloak/service/FormServiceImpl.java b/forms/src/main/java/org/keycloak/service/FormServiceImpl.java
index 41482ad..a892454 100644
--- a/forms/src/main/java/org/keycloak/service/FormServiceImpl.java
+++ b/forms/src/main/java/org/keycloak/service/FormServiceImpl.java
@@ -59,13 +59,17 @@ public class FormServiceImpl implements FormService {
         commandMap.put(Pages.LOGIN, new CommandLogin());
         commandMap.put(Pages.REGISTER, new CommandRegister());
         commandMap.put(Pages.ACCOUNT, new CommandAccount());
+        commandMap.put(Pages.LOGIN_UPDATE_PROFILE, new CommandPassword());
         commandMap.put(Pages.PASSWORD, new CommandPassword());
+        commandMap.put(Pages.LOGIN_RESET_PASSWORD, new CommandPassword());
+        commandMap.put(Pages.LOGIN_UPDATE_PASSWORD, new CommandPassword());
         commandMap.put(Pages.ACCESS, new CommandAccess());
-        commandMap.put(Pages.LOGIN_TOTP, new CommandLoginTotp());
         commandMap.put(Pages.SECURITY_FAILURE, new CommandSecurityFailure());
         commandMap.put(Pages.SOCIAL, new CommandSocial());
         commandMap.put(Pages.TOTP, new CommandTotp());
-        commandMap.put(Pages.VERIFY_EMAIL, new CommandEmail());
+        commandMap.put(Pages.LOGIN_CONFIG_TOTP, new CommandTotp());
+        commandMap.put(Pages.LOGIN_TOTP, new CommandLoginTotp());
+        commandMap.put(Pages.LOGIN_VERIFY_EMAIL, new CommandLoginTotp());
     }
 
     public String getId(){
diff --git a/forms/src/main/resources/META-INF/resources/forms/login-config-totp.ftl b/forms/src/main/resources/META-INF/resources/forms/login-config-totp.ftl
new file mode 100644
index 0000000..15fbecc
--- /dev/null
+++ b/forms/src/main/resources/META-INF/resources/forms/login-config-totp.ftl
@@ -0,0 +1 @@
+<#include "./theme/" + template.theme + "/login-config-totp.ftl">
\ No newline at end of file
diff --git a/forms/src/main/resources/META-INF/resources/forms/login-reset-password.ftl b/forms/src/main/resources/META-INF/resources/forms/login-reset-password.ftl
new file mode 100644
index 0000000..69052d0
--- /dev/null
+++ b/forms/src/main/resources/META-INF/resources/forms/login-reset-password.ftl
@@ -0,0 +1 @@
+<#include "./theme/" + template.theme + "/login-reset-password.ftl">
\ No newline at end of file
diff --git a/forms/src/main/resources/META-INF/resources/forms/login-update-password.ftl b/forms/src/main/resources/META-INF/resources/forms/login-update-password.ftl
new file mode 100644
index 0000000..fcf2e69
--- /dev/null
+++ b/forms/src/main/resources/META-INF/resources/forms/login-update-password.ftl
@@ -0,0 +1 @@
+<#include "./theme/" + template.theme + "/login-update-password.ftl">
\ No newline at end of file
diff --git a/forms/src/main/resources/META-INF/resources/forms/login-update-profile.ftl b/forms/src/main/resources/META-INF/resources/forms/login-update-profile.ftl
new file mode 100644
index 0000000..c05cd6c
--- /dev/null
+++ b/forms/src/main/resources/META-INF/resources/forms/login-update-profile.ftl
@@ -0,0 +1 @@
+<#include "./theme/" + template.theme + "/login-update-profile.ftl">
\ No newline at end of file
diff --git a/forms/src/main/resources/META-INF/resources/forms/login-verify-email.ftl b/forms/src/main/resources/META-INF/resources/forms/login-verify-email.ftl
new file mode 100644
index 0000000..64af784
--- /dev/null
+++ b/forms/src/main/resources/META-INF/resources/forms/login-verify-email.ftl
@@ -0,0 +1 @@
+<#include "./theme/" + template.theme + "/login-verify-email.ftl">
\ No newline at end of file
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login.ftl
index 80994d6..e74b40f 100755
--- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login.ftl
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login.ftl
@@ -36,6 +36,8 @@
         <#if realm.registrationAllowed>
             <p>${rb.getString('noAccount')} <a href="${url.registrationUrl?default('')}">${rb.getString('register')}</a>.</p>
         </#if>
+        
+        <a href="${url.passwordResetUrl}">Reset password</a>
     </div>
 
     </#if>
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-config-totp.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-config-totp.ftl
new file mode 100755
index 0000000..bceee99
--- /dev/null
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-config-totp.ftl
@@ -0,0 +1,42 @@
+<#import "template-login-action.ftl" as layout>
+<@layout.registrationLayout bodyClass=""; section>
+    <#if section = "title">
+
+    Config TOTP
+
+    <#elseif section = "header">
+
+    Config TOTP
+
+    <#elseif section = "form">
+
+    <div name="form">
+        <h2>To setup Google Authenticator</h2>
+
+        <ol>
+            <li>Install Google Authenticator to your device</li>
+            <li>Set up an account in Google Authenticator and scan the QR code below or enter the key<br />
+                <img src="${totp.totpSecretQrCodeUrl}" /> ${totp.totpSecretEncoded}
+            </li>
+            <li>Enter a one-time password provided by Google Authenticator and click Save to finish the setup
+
+                <form action="${url.totpUrl}" method="post">
+                    <div>
+                        <label for="totp">${rb.getString('authenticatorCode')}</label>
+                        <input type="text" id="totp" name="totp" />
+                        <input type="hidden" id="totpSecret" name="totpSecret" value="${totp.totpSecret}" />
+                    </div>
+
+                    <input type="submit" value="Submit" />
+                </form>
+            </li>
+        </ol>
+    </div>
+
+    <#elseif section = "info" >
+
+    <div name="info">
+    </div>
+
+    </#if>
+</@layout.registrationLayout>
\ No newline at end of file
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-reset-password.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-reset-password.ftl
new file mode 100755
index 0000000..2e3a715
--- /dev/null
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-reset-password.ftl
@@ -0,0 +1,34 @@
+<#import "template-login-action.ftl" as layout>
+<@layout.registrationLayout bodyClass=""; section>
+    <#if section = "title">
+
+    Reset password
+
+    <#elseif section = "header">
+
+    Reset password
+
+    <#elseif section = "form">
+
+    <div name="form">
+        <form action="${url.passwordResetUrl}" method="post">
+            <div>
+                <label for="username">${rb.getString('username')}</label>
+                <input id="username" name="username" type="text" />
+            </div>
+        	<div>
+      	    	<label for="email">${rb.getString('email')}</label>
+            	<input type="text" id="email" name="email" />
+			</div>
+
+            <input type="submit" value="Submit" />
+        </form>
+    </div>
+
+    <#elseif section = "info" >
+
+    <div name="info">
+    </div>
+
+    </#if>
+</@layout.registrationLayout>
\ No newline at end of file
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.ftl
index 5ca1109..88e61a0 100755
--- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.ftl
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.ftl
@@ -1,4 +1,4 @@
-<#import "template-login.ftl" as layout>
+<#import "template-login-action.ftl" as layout>
 <@layout.registrationLayout bodyClass=""; section>
 
     <#if section = "title">
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-password.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-password.ftl
new file mode 100755
index 0000000..fdfcb2c
--- /dev/null
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-password.ftl
@@ -0,0 +1,34 @@
+<#import "template-login-action.ftl" as layout>
+<@layout.registrationLayout bodyClass=""; section>
+    <#if section = "title">
+
+    Update password
+
+    <#elseif section = "header">
+
+    Update password
+
+    <#elseif section = "form">
+
+    <div name="form">
+        <form action="${url.passwordUrl}" method="post">
+        	<div>
+            	<label for="password-new">${rb.getString('passwordNew')}</label>
+            	<input type="password" id="password-new" name="password-new" />
+        	</div>
+        	<div>
+        	    <label for="password-confirm">${rb.getString('passwordConfirm')}</label>
+    	        <input type="password" id="password-confirm" name="password-confirm" />
+	        </div>
+
+            <input type="submit" value="Submit" />
+        </form>
+    </div>
+
+    <#elseif section = "info" >
+
+    <div name="info">
+    </div>
+
+    </#if>
+</@layout.registrationLayout>
\ No newline at end of file
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-profile.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-profile.ftl
new file mode 100755
index 0000000..84bab40
--- /dev/null
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-profile.ftl
@@ -0,0 +1,41 @@
+<#import "template-login-action.ftl" as layout>
+<@layout.registrationLayout bodyClass=""; section>
+    <#if section = "title">
+
+    Update profile
+
+    <#elseif section = "header">
+
+    Update profile
+
+    <#elseif section = "form">
+
+    <div name="form">
+        <form action="${url.accountUrl}" method="post">
+        	<div>
+            	<label for="firstName">${rb.getString('firstName')}</label>
+	            <input type="text" id="firstName" name="firstName" value="${user.firstName?default('')}" />
+    	    </div>
+	        <div>
+    	        <label for="lastName">${rb.getString('lastName')}</label>
+            	<input type="text" id="lastName" name="lastName" value="${user.lastName?default('')}" />
+        	</div>
+	        <div>
+    	        <label for="email">${rb.getString('email')}</label>
+            	<input type="text" id="email" name="email" value="${user.email?default('')}" />
+	        </div>
+
+            <div class="aside-btn">
+            </div>
+
+            <input type="submit" value="Submit" />
+        </form>
+    </div>
+
+    <#elseif section = "info" >
+
+    <div name="info">
+    </div>
+
+    </#if>
+</@layout.registrationLayout>
\ No newline at end of file
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-verify-email.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-verify-email.ftl
new file mode 100755
index 0000000..bd2b141
--- /dev/null
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-verify-email.ftl
@@ -0,0 +1,24 @@
+<#import "template-login-action.ftl" as layout>
+<@layout.registrationLayout bodyClass=""; section>
+    <#if section = "title">
+
+    Verify email
+
+    <#elseif section = "header">
+
+    Verify email
+
+    <#elseif section = "form">
+
+    <div name="form">
+    	An email with instructions to verify your email address has been sent to you. If you don't receive this email, 
+    	<a href="${url.emailVerificationUrl}">click here</a> to re-send the email.
+    </div>
+
+    <#elseif section = "info" >
+
+    <div name="info">
+    </div>
+
+    </#if>
+</@layout.registrationLayout>
\ No newline at end of file
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login-action.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login-action.ftl
new file mode 100644
index 0000000..2b6074b
--- /dev/null
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login-action.ftl
@@ -0,0 +1,63 @@
+<#macro registrationLayout bodyClass>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html">
+
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <title>
+        <#nested "title">
+    </title>
+    <link href="${template.themeConfig.styles}" rel="stylesheet" />
+    <style>
+        body {
+            background-image: url("${template.themeConfig.background}");
+        }
+    </style>
+</head>
+
+<body class="rcue-login-register ${bodyClass}">
+    <#if (template.themeConfig.logo)?has_content>
+        <h1>
+            <a href="#" title="Go to the home page"><img src="${template.themeConfig.logo}" alt="Logo" /></a>
+        </h1>
+    </#if>
+
+    <div class="content">
+        <h2>
+            <#nested "header">
+        </h2>
+
+        <div class="background-area">
+            <div class="form-area clearfix">
+                <section class="app-form">
+                    <h3>Application login area</h3>
+                    <#nested "form">
+                </section>
+
+                <#if error?has_content>
+                    <div class="feedback error bottom-left show">
+                        <p>
+                            <strong id="loginError">${rb.getString(error.summary)}</strong>
+                        </p>
+                    </div>
+                </#if>
+
+                <section class="info-area">
+                    <h3>Info area</h3>
+                    <#nested "info">
+                </section>
+            </div>
+        </div>
+
+        <#if template.themeConfig['displayPoweredBy']>
+            <p class="powered">
+                <a href="#">${rb.getString('poweredByKeycloak')}</a>
+            </p>
+        </#if>
+    </div>
+
+    <#nested "content">
+
+</body>
+</html>
+</#macro>
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/services/email/EmailSender.java b/services/src/main/java/org/keycloak/services/email/EmailSender.java
index 8da383a..cdf52d8 100644
--- a/services/src/main/java/org/keycloak/services/email/EmailSender.java
+++ b/services/src/main/java/org/keycloak/services/email/EmailSender.java
@@ -23,9 +23,8 @@ package org.keycloak.services.email;
 
 import java.net.URI;
 import java.util.Map.Entry;
-import java.util.concurrent.TimeUnit;
-import java.util.List;
 import java.util.Properties;
+import java.util.concurrent.TimeUnit;
 
 import javax.mail.Message;
 import javax.mail.MessagingException;
@@ -38,6 +37,7 @@ import javax.ws.rs.core.UriBuilder;
 import javax.ws.rs.core.UriInfo;
 
 import org.jboss.resteasy.logging.Logger;
+import org.keycloak.services.managers.AccessCodeEntry;
 import org.keycloak.services.models.RealmModel;
 import org.keycloak.services.models.UserModel;
 import org.keycloak.services.resources.AccountService;
@@ -78,12 +78,9 @@ public class EmailSender {
         transport.sendMessage(msg, new InternetAddress[] { new InternetAddress(address) });
     }
 
-    public void sendEmailVerification(UserModel user, RealmModel realm, String code, UriInfo uriInfo) {
-        UriBuilder builder = Urls.accountBase(uriInfo.getBaseUri()).path(AccountService.class, "processEmailVerification");
-        for (Entry<String, List<String>> e : uriInfo.getQueryParameters().entrySet()) {
-            builder.queryParam(e.getKey(), e.getValue().toArray());
-        }
-        builder.queryParam("code", code);
+    public void sendEmailVerification(UserModel user, RealmModel realm, AccessCodeEntry accessCode, UriInfo uriInfo) {
+        UriBuilder builder = Urls.accountBase(uriInfo.getBaseUri()).path(AccountService.class, "emailVerification");
+        builder.queryParam("key", accessCode.getId());
 
         URI uri = builder.build(realm.getId());
 
@@ -99,12 +96,9 @@ public class EmailSender {
         }
     }
 
-    public void sendPasswordReset(UserModel user, RealmModel realm, String code, UriInfo uriInfo) {
+    public void sendPasswordReset(UserModel user, RealmModel realm, AccessCodeEntry accessCode, UriInfo uriInfo) {
         UriBuilder builder = Urls.accountBase(uriInfo.getBaseUri()).path(AccountService.class, "passwordPage");
-        for (Entry<String, List<String>> e : uriInfo.getQueryParameters().entrySet()) {
-            builder.queryParam(e.getKey(), e.getValue().toArray());
-        }
-        builder.queryParam("code", code);
+        builder.queryParam("key", accessCode.getId());
 
         URI uri = builder.build(realm.getId());
 
diff --git a/services/src/main/java/org/keycloak/services/models/UserModel.java b/services/src/main/java/org/keycloak/services/models/UserModel.java
index 9bd370a..2e6d77f 100755
--- a/services/src/main/java/org/keycloak/services/models/UserModel.java
+++ b/services/src/main/java/org/keycloak/services/models/UserModel.java
@@ -54,6 +54,6 @@ public interface UserModel {
     void setTotp(boolean totp);
 
     public static enum RequiredAction {
-        VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, RESET_PASSWORD
+        VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, UPDATE_PASSWORD
     }
 }
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java
index 231aed4..c07213d 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -28,7 +28,6 @@ import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
-import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
@@ -45,7 +44,6 @@ import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.services.email.EmailSender;
 import org.keycloak.services.managers.AccessCodeEntry;
 import org.keycloak.services.managers.AuthenticationManager;
-import org.keycloak.services.managers.ResourceAdminManager;
 import org.keycloak.services.managers.TokenManager;
 import org.keycloak.services.messages.Messages;
 import org.keycloak.services.models.RealmModel;
@@ -54,7 +52,6 @@ import org.keycloak.services.models.UserModel;
 import org.keycloak.services.models.UserModel.RequiredAction;
 import org.keycloak.services.resources.flows.Flows;
 import org.keycloak.services.resources.flows.FormFlows;
-import org.keycloak.services.resources.flows.OAuthFlows;
 import org.keycloak.services.validation.Validation;
 import org.picketlink.idm.credential.util.TimeBasedOTP;
 
@@ -116,9 +113,8 @@ public class AccountService {
             accessCodeEntry.getRequiredActions().remove(UserModel.RequiredAction.UPDATE_PROFILE);
         }
 
-        Response response = redirectOauth(user, accessCodeEntry);
-        if (response != null) {
-            return response;
+        if (accessCodeEntry != null) {
+            return redirectOauth(user, accessCodeEntry);
         } else {
             return Flows.forms(realm, request, uriInfo).setUser(user).forwardToAccount();
         }
@@ -207,34 +203,88 @@ public class AccountService {
 
         user.setTotp(true);
 
-        Response response = redirectOauth(user, accessCodeEntry);
-        if (response != null) {
-            return response;
+        if (accessCodeEntry != null) {
+            return redirectOauth(user, accessCodeEntry);
         } else {
             return Flows.forms(realm, request, uriInfo).setUser(user).forwardToTotp();
         }
     }
 
-    @Path("email-verify")
+    @Path("password-reset")
     @GET
-    public Response processEmailVerification() {
-        AccessCodeEntry accessCodeEntry = getAccessCodeEntry(RequiredAction.VERIFY_EMAIL);
-        UserModel user = accessCodeEntry != null ? getUserFromAccessCode(accessCodeEntry) : null;
-        if (user == null) {
-            return Response.status(Status.FORBIDDEN).build();
+    public Response passwordReset() {
+        return Flows.forms(realm, request, uriInfo).forwardToPasswordReset();
+    }
+
+    @Path("password-reset")
+    @POST
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response sendPasswordReset(final MultivaluedMap<String, String> formData) {
+        String username = formData.getFirst("username");
+        String email = formData.getFirst("email");
+
+        String scopeParam = uriInfo.getQueryParameters().getFirst("scope");
+        String state = uriInfo.getQueryParameters().getFirst("state");
+        String redirect = uriInfo.getQueryParameters().getFirst("redirect_uri");
+        String clientId = uriInfo.getQueryParameters().getFirst("client_id");
+
+        UserModel client = realm.getUser(clientId);
+        if (client == null) {
+            return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).forwardToSecurityFailure(
+                    "Unknown login requester.");
+        }
+        if (!client.isEnabled()) {
+            return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).forwardToSecurityFailure(
+                    "Login requester not enabled.");
         }
 
-        user.setEmailVerified(true);
-        user.removeRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
-        if (accessCodeEntry != null) {
-            accessCodeEntry.getRequiredActions().remove(UserModel.RequiredAction.VERIFY_EMAIL);
+        UserModel user = realm.getUser(username);
+        if (user == null || !email.equals(user.getEmail())) {
+            Flows.forms(realm, request, uriInfo).setError("Invalid username or email")
+                    .forwardToAction(RequiredAction.UPDATE_PASSWORD);
         }
 
-        Response response = redirectOauth(user, accessCodeEntry);
-        if (response != null) {
-            return response;
+        Set<RequiredAction> requiredActions = new HashSet<RequiredAction>(user.getRequiredActions());
+        requiredActions.add(RequiredAction.UPDATE_PASSWORD);
+
+        AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user);
+        accessCode.setRequiredActions(requiredActions);
+        accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction());
+
+        new EmailSender().sendPasswordReset(user, realm, accessCode, uriInfo);
+
+        return Flows.forms(realm, request, uriInfo).forwardToPasswordReset();
+    }
+
+    @Path("email-verification")
+    @GET
+    public Response emailVerification() {
+        if (uriInfo.getQueryParameters().containsKey("key")) {
+            AccessCodeEntry accessCode = tokenManager.getAccessCode(uriInfo.getQueryParameters().getFirst("key"));
+            if (accessCode == null || accessCode.isExpired()
+                    || !accessCode.getRequiredActions().contains(RequiredAction.VERIFY_EMAIL)) {
+                return Response.status(Status.FORBIDDEN).build();
+            }
+
+            String loginName = accessCode.getUser().getLoginName();
+            UserModel user = realm.getUser(loginName);
+            user.setEmailVerified(true);
+            user.removeRequiredAction(RequiredAction.VERIFY_EMAIL);
+
+            accessCode.getRequiredActions().remove(RequiredAction.VERIFY_EMAIL);
+
+            return redirectOauth(user, accessCode);
         } else {
-            return Flows.forms(realm, request, uriInfo).setUser(user).forwardToVerifyEmail();
+            AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.VERIFY_EMAIL);
+            UserModel user = accessCode != null ? getUserFromAccessCode(accessCode) : null;
+            if (user == null) {
+                return Response.status(Status.FORBIDDEN).build();
+            }
+
+            new EmailSender().sendEmailVerification(user, realm, accessCode, uriInfo);
+
+            return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(user)
+                    .forwardToAction(RequiredAction.VERIFY_EMAIL);
         }
     }
 
@@ -245,17 +295,11 @@ public class AccountService {
 
         Set<RequiredAction> requiredActions = user.getRequiredActions();
         if (!requiredActions.isEmpty()) {
-            return Flows.forms(realm, request, uriInfo).setCode(accessCode.getCode()).setUser(user)
+            return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(user)
                     .forwardToAction(requiredActions.iterator().next());
         } else {
-            String redirect = uriInfo.getQueryParameters().getFirst("redirect_uri");
-            if (redirect != null) {
-                String state = uriInfo.getQueryParameters().getFirst("state");
-                return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode, state,
-                        redirect);
-            } else {
-                return null;
-            }
+            return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode,
+                    accessCode.getState(), accessCode.getRedirectUri());
         }
     }
 
@@ -263,12 +307,14 @@ public class AccountService {
     @POST
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
     public Response processPasswordUpdate(final MultivaluedMap<String, String> formData) {
-        AccessCodeEntry accessCodeEntry = getAccessCodeEntry(RequiredAction.RESET_PASSWORD);
-        UserModel user = accessCodeEntry != null ? getUserFromAccessCode(accessCodeEntry) : getUserFromAuthManager();
+        AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.UPDATE_PASSWORD);
+        UserModel user = accessCode != null ? getUserFromAccessCode(accessCode) : getUserFromAuthManager();
         if (user == null) {
             return Response.status(Status.FORBIDDEN).build();
         }
 
+        boolean loginAction = accessCode != null;
+
         FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user);
 
         String password = formData.getFirst("password");
@@ -283,7 +329,7 @@ public class AccountService {
             error = Messages.INVALID_PASSWORD_CONFIRM;
         }
 
-        if (accessCodeEntry == null) {
+        if (!loginAction) {
             if (Validation.isEmpty(password)) {
                 error = Messages.MISSING_PASSWORD;
             } else if (!realm.validatePassword(user, password)) {
@@ -301,15 +347,16 @@ public class AccountService {
 
         realm.updateCredential(user, credentials);
 
-        user.removeRequiredAction(RequiredAction.RESET_PASSWORD);
-        if (accessCodeEntry != null) {
-            accessCodeEntry.getRequiredActions().remove(UserModel.RequiredAction.RESET_PASSWORD);
+        user.removeRequiredAction(RequiredAction.UPDATE_PASSWORD);
+        if (accessCode != null) {
+            accessCode.getRequiredActions().remove(UserModel.RequiredAction.UPDATE_PASSWORD);
         }
 
-        authManager.expireIdentityCookie(realm, uriInfo);
-        new ResourceAdminManager().singleLogOut(realm, user.getLoginName());
-
-        return Flows.forms(realm, request, uriInfo).forwardToLogin();
+        if (accessCode != null) {
+            return redirectOauth(user, accessCode);
+        } else {
+            return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword();
+        }
     }
 
     @Path("")
@@ -348,61 +395,21 @@ public class AccountService {
     @Path("password")
     @GET
     public Response passwordPage() {
-        UserModel user = getUserFromAuthManager();
-
-        // TODO Remove when we have a separate login-reset-password page
-        if (user == null) {
-            AccessCodeEntry accessCodeEntry = getAccessCodeEntry(RequiredAction.RESET_PASSWORD);
-            user = accessCodeEntry != null ? getUserFromAccessCode(accessCodeEntry) : null;
-        }
+        if (uriInfo.getQueryParameters().containsKey("key")) {
+            AccessCodeEntry accessCode = tokenManager.getAccessCode(uriInfo.getQueryParameters().getFirst("key"));
+            if (accessCode == null || accessCode.isExpired()
+                    || !accessCode.getRequiredActions().contains(RequiredAction.UPDATE_PASSWORD)) {
+                return Response.status(Status.FORBIDDEN).build();
+            }
 
-        if (user != null) {
-            return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword();
+            return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode)
+                    .forwardToAction(RequiredAction.UPDATE_PASSWORD);
         } else {
-            return Response.status(Status.FORBIDDEN).build();
-        }
-    }
-
-    @Path("password-reset")
-    @GET
-    public Response resetPassword(@QueryParam("username") final String username,
-            @QueryParam("client_id") final String clientId, @QueryParam("scope") final String scopeParam,
-            @QueryParam("state") final String state, @QueryParam("redirect_uri") final String redirect) {
-
-        OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
-
-        if (!realm.isEnabled()) {
-            return oauth.forwardToSecurityFailure("Realm not enabled.");
-        }
-        if (!realm.isResetPasswordAllowed()) {
-            return oauth.forwardToSecurityFailure("Password reset not permitted, contact admin.");
-        }
-
-        UserModel client = realm.getUser(clientId);
-        if (client == null) {
-            return oauth.forwardToSecurityFailure("Unknown login requester.");
-        }
-        if (!client.isEnabled()) {
-            return oauth.forwardToSecurityFailure("Login requester not enabled.");
-        }
-
-        // String username = formData.getFirst("username");
-        UserModel user = realm.getUser(username);
-
-        Set<RequiredAction> requiredActions = new HashSet<RequiredAction>(user.getRequiredActions());
-        requiredActions.add(RequiredAction.RESET_PASSWORD);
-
-        AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user);
-        accessCode.setRequiredActions(requiredActions);
-        accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction());
-
-        if (user.getEmail() == null) {
-            return oauth.forwardToSecurityFailure("Email address not set, contact admin");
+            UserModel user = getUserFromAuthManager();
+            if (user == null) {
+                return Response.status(Status.FORBIDDEN).build();
+            }
+            return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword();
         }
-
-        new EmailSender().sendPasswordReset(user, realm, accessCode.getCode(), uriInfo);
-        // TODO Add info message
-        return Flows.forms(realm, request, uriInfo).forwardToLogin();
     }
-
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java
index c33be74..a0996ab 100755
--- a/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java
@@ -28,6 +28,7 @@ import org.jboss.resteasy.spi.HttpRequest;
 import org.jboss.resteasy.spi.ResteasyUriInfo;
 import org.keycloak.services.FormService;
 import org.keycloak.services.email.EmailSender;
+import org.keycloak.services.managers.AccessCodeEntry;
 import org.keycloak.services.models.RealmModel;
 import org.keycloak.services.models.UserModel;
 import org.keycloak.services.models.UserModel.RequiredAction;
@@ -60,7 +61,7 @@ public class FormFlows {
     private UserModel userModel;
 
     private boolean socialRegistration;
-    private String code;
+    private AccessCodeEntry accessCode;
     private UriInfo uriInfo;
 
     FormFlows(RealmModel realm, HttpRequest request, UriInfo uriInfo) {
@@ -72,15 +73,16 @@ public class FormFlows {
     public Response forwardToAction(RequiredAction action) {
         switch (action) {
             case CONFIGURE_TOTP:
-                return forwardToTotp();
+                return forwardToForm(Pages.LOGIN_CONFIG_TOTP);
             case UPDATE_PROFILE:
-                return forwardToAccount();
-            case RESET_PASSWORD:
-                return forwardToPassword();
+                return forwardToForm(Pages.LOGIN_UPDATE_PROFILE);
+            case UPDATE_PASSWORD:
+                return forwardToForm(Pages.LOGIN_UPDATE_PASSWORD);
             case VERIFY_EMAIL:
-                return forwardToVerifyEmail();
+                new EmailSender().sendEmailVerification(userModel, realm, accessCode, uriInfo);
+                return forwardToForm(Pages.LOGIN_VERIFY_EMAIL);
             default:
-                return null; // TODO
+                return Response.serverError().build();
         }
     }
 
@@ -107,8 +109,8 @@ public class FormFlows {
             uriBuilder.replaceQueryParam(k, queryParameterMap.get(k).toArray());
         }
 
-        if (code != null){
-            uriBuilder.queryParam(CODE, code);
+        if (accessCode != null) {
+            uriBuilder.queryParam(CODE, accessCode.getCode());
         }
 
         URI baseURI = uriBuilder.build();
@@ -135,6 +137,10 @@ public class FormFlows {
         return forwardToForm(Pages.LOGIN);
     }
 
+    public Response forwardToPasswordReset() {
+        return forwardToForm(Pages.LOGIN_RESET_PASSWORD);
+    }
+
     public Response forwardToLoginTotp() {
         return forwardToForm(Pages.LOGIN_TOTP);
     }
@@ -155,13 +161,8 @@ public class FormFlows {
         return forwardToForm(Pages.TOTP);
     }
 
-    public Response forwardToVerifyEmail() {
-        new EmailSender().sendEmailVerification(userModel, realm, code, uriInfo);
-        return forwardToForm(Pages.VERIFY_EMAIL);
-    }
-
-    public FormFlows setCode(String code) {
-        this.code = code;
+    public FormFlows setAccessCode(AccessCodeEntry accessCode) {
+        this.accessCode = accessCode;
         return this;
     }
 
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java
index cc76bd0..25ba3d8 100755
--- a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java
@@ -97,7 +97,7 @@ public class OAuthFlows {
         if (!requiredActions.isEmpty()) {
             accessCode.setRequiredActions(new HashSet<UserModel.RequiredAction>(requiredActions));
             accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction());
-            return Flows.forms(realm, request, uriInfo).setCode(accessCode.getCode()).setUser(user)
+            return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(user)
                     .forwardToAction(user.getRequiredActions().iterator().next());
         }
 
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Pages.java b/services/src/main/java/org/keycloak/services/resources/flows/Pages.java
index 123b1dd..a5f913d 100644
--- a/services/src/main/java/org/keycloak/services/resources/flows/Pages.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/Pages.java
@@ -34,10 +34,18 @@ public class Pages {
 
     public final static String LOGIN_TOTP = "/forms/login-totp.ftl";
 
+    public final static String LOGIN_CONFIG_TOTP = "/forms/login-config-totp.ftl";
+
+    public final static String LOGIN_VERIFY_EMAIL = "/forms/login-verify-email.ftl";
+
     public final static String OAUTH_GRANT = "/saas/oauthGrantForm.jsp";
 
     public final static String PASSWORD = "/forms/password.ftl";
 
+    public final static String LOGIN_RESET_PASSWORD = "/forms/login-reset-password.ftl";
+
+    public final static String LOGIN_UPDATE_PASSWORD = "/forms/login-update-password.ftl";
+
     public final static String REGISTER = "/forms/register.ftl";
 
     public final static String SECURITY_FAILURE = "/saas/securityFailure.jsp";
@@ -46,6 +54,6 @@ public class Pages {
 
     public final static String TOTP = "/forms/totp.ftl";
 
-    public final static String VERIFY_EMAIL = "/forms/verify-email.ftl";
+    public final static String LOGIN_UPDATE_PROFILE = "/forms/login-update-profile.ftl";
 
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java
index b797ff4..1da6a61 100755
--- a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java
@@ -55,6 +55,14 @@ public class Urls {
         return accountBase(baseUri).path(AccountService.class, "totpPage").build(realmId);
     }
 
+    public static URI accountEmailVerification(URI baseUri, String realmId) {
+        return accountBase(baseUri).path(AccountService.class, "emailVerification").build(realmId);
+    }
+
+    public static URI accountPasswordReset(URI baseUri, String realmId) {
+        return accountBase(baseUri).path(AccountService.class, "passwordReset").build(realmId);
+    }
+
     private static UriBuilder realmBase(URI baseUri) {
         return UriBuilder.fromUri(baseUri).path(RealmsResource.class);
     }
diff --git a/testsuite/src/test/java/org/keycloak/testsuite/AccountTest.java b/testsuite/src/test/java/org/keycloak/testsuite/AccountTest.java
index 9fef302..6321031 100644
--- a/testsuite/src/test/java/org/keycloak/testsuite/AccountTest.java
+++ b/testsuite/src/test/java/org/keycloak/testsuite/AccountTest.java
@@ -51,6 +51,8 @@ public class AccountTest extends AbstractDroneTest {
         changePasswordPage.changePassword("password", "new-password", "new-password");
 
         appPage.open();
+        Assert.assertTrue(appPage.isCurrent());
+        appPage.logout();
 
         Assert.assertTrue(loginPage.isCurrent());
 
diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/ChangePasswordPage.java b/testsuite/src/test/java/org/keycloak/testsuite/pages/ChangePasswordPage.java
index 1e0185f..3b17f9a 100644
--- a/testsuite/src/test/java/org/keycloak/testsuite/pages/ChangePasswordPage.java
+++ b/testsuite/src/test/java/org/keycloak/testsuite/pages/ChangePasswordPage.java
@@ -25,13 +25,6 @@ public class ChangePasswordPage {
     @FindBy(css = "input[type=\"submit\"]")
     private WebElement submitButton;
 
-    public void changePassword(String newPassword, String passwordConfirm) {
-        newPasswordInput.sendKeys(newPassword);
-        passwordConfirmInput.sendKeys(passwordConfirm);
-
-        submitButton.click();
-    }
-
     public void changePassword(String password, String newPassword, String passwordConfirm) {
         passwordInput.sendKeys(password);
         newPasswordInput.sendKeys(newPassword);
diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginConfigTotpPage.java b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginConfigTotpPage.java
new file mode 100644
index 0000000..1c7c062
--- /dev/null
+++ b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginConfigTotpPage.java
@@ -0,0 +1,42 @@
+package org.keycloak.testsuite.pages;
+
+import org.jboss.arquillian.drone.api.annotation.Drone;
+import org.keycloak.testsuite.Constants;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+public class LoginConfigTotpPage {
+
+    private static String PATH = Constants.AUTH_SERVER_ROOT + "/rest/realms/demo/account/totp";
+
+    @Drone
+    private WebDriver browser;
+
+    @FindBy(id = "totpSecret")
+    private WebElement totpSecret;
+
+    @FindBy(id = "totp")
+    private WebElement totpInput;
+
+    @FindBy(css = "input[type=\"submit\"]")
+    private WebElement submitButton;
+
+    public void configure(String totp) {
+        totpInput.sendKeys(totp);
+        submitButton.click();
+    }
+
+    public String getTotpSecret() {
+        return totpSecret.getAttribute("value");
+    }
+
+    public boolean isCurrent() {
+        return browser.getTitle().equals("Config TOTP");
+    }
+
+    public void open() {
+        browser.navigate().to(PATH);
+    }
+
+}
diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPage.java b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPage.java
index 0e4a549..c639bdd 100644
--- a/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPage.java
+++ b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPage.java
@@ -22,6 +22,9 @@ public class LoginPage {
     @FindBy(linkText = "Register")
     private WebElement registerLink;
 
+    @FindBy(linkText = "Reset password")
+    private WebElement resetPasswordLink;
+
     @FindBy(id = "loginError")
     private WebElement loginErrorMessage;
 
@@ -47,4 +50,8 @@ public class LoginPage {
         registerLink.click();
     }
 
+    public void resetPassword() {
+        resetPasswordLink.click();
+    }
+
 }
diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java
new file mode 100644
index 0000000..7bd9836
--- /dev/null
+++ b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java
@@ -0,0 +1,40 @@
+package org.keycloak.testsuite.pages;
+
+import org.jboss.arquillian.drone.api.annotation.Drone;
+import org.keycloak.testsuite.Constants;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+public class LoginPasswordResetPage {
+
+    private static String PATH = Constants.AUTH_SERVER_ROOT + "/rest/realms/demo/account/password";
+
+    @Drone
+    private WebDriver browser;
+
+    @FindBy(id = "username")
+    private WebElement usernameInput;
+
+    @FindBy(id = "email")
+    private WebElement emailInput;
+
+    @FindBy(css = "input[type=\"submit\"]")
+    private WebElement submitButton;
+
+    public void changePassword(String username, String email) {
+        usernameInput.sendKeys(username);
+        emailInput.sendKeys(email);
+
+        submitButton.click();
+    }
+
+    public boolean isCurrent() {
+        return browser.getTitle().equals("Reset password");
+    }
+
+    public void open() {
+        browser.navigate().to(PATH);
+    }
+
+}
diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java
new file mode 100644
index 0000000..2a19f0c
--- /dev/null
+++ b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java
@@ -0,0 +1,40 @@
+package org.keycloak.testsuite.pages;
+
+import org.jboss.arquillian.drone.api.annotation.Drone;
+import org.keycloak.testsuite.Constants;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+public class LoginPasswordUpdatePage {
+
+    private static String PATH = Constants.AUTH_SERVER_ROOT + "/rest/realms/demo/account/password";
+
+    @Drone
+    private WebDriver browser;
+
+    @FindBy(id = "password-new")
+    private WebElement newPasswordInput;
+
+    @FindBy(id = "password-confirm")
+    private WebElement passwordConfirmInput;
+
+    @FindBy(css = "input[type=\"submit\"]")
+    private WebElement submitButton;
+
+    public void changePassword(String newPassword, String passwordConfirm) {
+        newPasswordInput.sendKeys(newPassword);
+        passwordConfirmInput.sendKeys(passwordConfirm);
+
+        submitButton.click();
+    }
+
+    public boolean isCurrent() {
+        return browser.getTitle().equals("Update password");
+    }
+
+    public void open() {
+        browser.navigate().to(PATH);
+    }
+
+}
diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java
new file mode 100644
index 0000000..2f37961
--- /dev/null
+++ b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java
@@ -0,0 +1,44 @@
+package org.keycloak.testsuite.pages;
+
+import org.jboss.arquillian.drone.api.annotation.Drone;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+public class LoginUpdateProfilePage {
+
+    @Drone
+    private WebDriver browser;
+
+    @FindBy(id = "firstName")
+    private WebElement firstNameInput;
+
+    @FindBy(id = "lastName")
+    private WebElement lastNameInput;
+
+    @FindBy(id = "email")
+    private WebElement emailInput;
+
+    @FindBy(css = "input[type=\"submit\"]")
+    private WebElement submitButton;
+
+    @FindBy(id = "loginError")
+    private WebElement loginErrorMessage;
+
+    public void update(String firstName, String lastName, String email) {
+        firstNameInput.sendKeys(firstName);
+        lastNameInput.sendKeys(lastName);
+        emailInput.sendKeys(email);
+
+        submitButton.click();
+    }
+
+    public String getError() {
+        return loginErrorMessage != null ? loginErrorMessage.getText() : null;
+    }
+
+    public boolean isCurrent() {
+        return browser.getTitle().equals("Update profile");
+    }
+
+}
diff --git a/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionEmailVerificationTest.java b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionEmailVerificationTest.java
index 6ca1a01..134a6b1 100644
--- a/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionEmailVerificationTest.java
+++ b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionEmailVerificationTest.java
@@ -22,6 +22,8 @@
 package org.keycloak.testsuite;
 
 import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import javax.mail.MessagingException;
 import javax.mail.internet.MimeMessage;
@@ -94,12 +96,17 @@ public class RequiredActionEmailVerificationTest {
         loginPage.register();
         registerPage.register("name", "email", "verifyEmail", "password", "password");
 
-        Assert.assertTrue(browser.getPageSource().contains("Please verify your email address"));
+        Assert.assertTrue(browser.getPageSource().contains("Verify email"));
 
         MimeMessage message = greenMail.getReceivedMessages()[0];
 
         String body = (String) message.getContent();
-        String verificationUrl = body.split("\n")[0];
+
+        Pattern p = Pattern.compile("(?s).*(http://[^\\s]*).*");
+        Matcher m = p.matcher(body);
+        m.matches();
+
+        String verificationUrl = m.group(1);
 
         browser.navigate().to(verificationUrl.trim());
 
diff --git a/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionMultipleActionsTest.java b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionMultipleActionsTest.java
new file mode 100644
index 0000000..1964ec7
--- /dev/null
+++ b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionMultipleActionsTest.java
@@ -0,0 +1,94 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.testsuite;
+
+import java.net.MalformedURLException;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.drone.api.annotation.Drone;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.arquillian.junit.Arquillian;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.RegisterPage;
+import org.keycloak.testsuite.pages.TotpPage;
+import org.openqa.selenium.WebDriver;
+import org.picketlink.idm.credential.util.TimeBasedOTP;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+@RunWith(Arquillian.class)
+public class RequiredActionMultipleActionsTest {
+
+    @Deployment(name = "app", testable = false, order = 2)
+    public static WebArchive appDeployment() {
+        return Deployments.appDeployment();
+    }
+
+    @Deployment(name = "auth-server", testable = false, order = 1)
+    public static WebArchive deployment() {
+        return Deployments.deployment().addAsResource("testrealm-totp.json", "META-INF/testrealm.json");
+    }
+
+    @Page
+    protected AppPage appPage;
+
+    @Drone
+    protected WebDriver browser;
+
+    @Page
+    protected TotpPage totpPage;
+
+    @Page
+    protected LoginPage loginPage;
+
+    @Page
+    protected RegisterPage registerPage;
+
+    protected TimeBasedOTP totp;
+
+    @Before
+    public void before() throws MalformedURLException {
+        totp = new TimeBasedOTP();
+    }
+
+    @After
+    public void after() {
+        appPage.open();
+        if (appPage.isCurrent()) {
+            appPage.logout();
+        }
+    }
+
+    @Test
+    public void setupTotp() {
+        Assert.fail("Not implemented");
+    }
+
+}
diff --git a/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionResetPasswordTest.java b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionResetPasswordTest.java
new file mode 100644
index 0000000..0f369a7
--- /dev/null
+++ b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionResetPasswordTest.java
@@ -0,0 +1,79 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.testsuite;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.arquillian.junit.Arquillian;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+@RunWith(Arquillian.class)
+public class RequiredActionResetPasswordTest extends AbstractDroneTest {
+
+    @Deployment(name = "properties", testable = false, order = 1)
+    public static WebArchive propertiesDeployment() {
+        return ShrinkWrap.create(WebArchive.class, "properties.war").addClass(SystemPropertiesSetter.class)
+                .addAsWebInfResource("web-properties-email-verfication.xml", "web.xml");
+    }
+
+    @Rule
+    public GreenMailRule greenMail = new GreenMailRule();
+
+    @Page
+    protected LoginPasswordUpdatePage changePasswordPage;
+
+    @Test
+    public void tempPassword() {
+        appPage.open();
+
+        Assert.assertTrue(loginPage.isCurrent());
+
+        loginPage.login("reset@pass.com", "temp-password");
+
+        Assert.assertTrue(changePasswordPage.isCurrent());
+
+        changePasswordPage.changePassword("new-password", "new-password");
+
+        Assert.assertTrue(appPage.isCurrent());
+        Assert.assertEquals("reset@pass.com", appPage.getUser());
+
+        appPage.logout();
+        appPage.open();
+
+        Assert.assertTrue(loginPage.isCurrent());
+
+        loginPage.login("reset@pass.com", "new-password");
+
+        Assert.assertTrue(appPage.isCurrent());
+        Assert.assertEquals("reset@pass.com", appPage.getUser());
+    }
+
+}
diff --git a/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionTotpSetupTest.java b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionTotpSetupTest.java
index 1742bcb..0d667ee 100644
--- a/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionTotpSetupTest.java
+++ b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionTotpSetupTest.java
@@ -34,9 +34,9 @@ import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.LoginConfigTotpPage;
 import org.keycloak.testsuite.pages.LoginPage;
 import org.keycloak.testsuite.pages.RegisterPage;
-import org.keycloak.testsuite.pages.TotpPage;
 import org.openqa.selenium.WebDriver;
 import org.picketlink.idm.credential.util.TimeBasedOTP;
 
@@ -63,7 +63,7 @@ public class RequiredActionTotpSetupTest {
     protected WebDriver browser;
 
     @Page
-    protected TotpPage totpPage;
+    protected LoginConfigTotpPage totpPage;
 
     @Page
     protected LoginPage loginPage;
diff --git a/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionUpdateProfileTest.java b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionUpdateProfileTest.java
new file mode 100644
index 0000000..74ae20a
--- /dev/null
+++ b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionUpdateProfileTest.java
@@ -0,0 +1,69 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.testsuite;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.arquillian.junit.Arquillian;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+@RunWith(Arquillian.class)
+public class RequiredActionUpdateProfileTest extends AbstractDroneTest {
+
+    @Deployment(name = "properties", testable = false, order = 1)
+    public static WebArchive propertiesDeployment() {
+        return ShrinkWrap.create(WebArchive.class, "properties.war").addClass(SystemPropertiesSetter.class)
+                .addAsWebInfResource("web-properties-email-verfication.xml", "web.xml");
+    }
+
+    @Rule
+    public GreenMailRule greenMail = new GreenMailRule();
+
+    @Page
+    protected LoginUpdateProfilePage updateProfilePage;
+
+    @Test
+    public void updateProfile() {
+        appPage.open();
+
+        Assert.assertTrue(loginPage.isCurrent());
+
+        loginPage.login("updateprof@pass.com", "password");
+
+        Assert.assertTrue(updateProfilePage.isCurrent());
+
+        updateProfilePage.update("New first", "New last", "new@email.com");
+
+        Assert.assertTrue(appPage.isCurrent());
+        Assert.assertEquals("updateprof@pass.com", appPage.getUser());
+    }
+
+}
diff --git a/testsuite/src/test/java/org/keycloak/testsuite/ResetPasswordTest.java b/testsuite/src/test/java/org/keycloak/testsuite/ResetPasswordTest.java
index 22b0ab7..755061e 100644
--- a/testsuite/src/test/java/org/keycloak/testsuite/ResetPasswordTest.java
+++ b/testsuite/src/test/java/org/keycloak/testsuite/ResetPasswordTest.java
@@ -35,7 +35,8 @@ import org.junit.Assert;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.keycloak.testsuite.pages.ChangePasswordPage;
+import org.keycloak.testsuite.pages.LoginPasswordResetPage;
+import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -53,7 +54,10 @@ public class ResetPasswordTest extends AbstractDroneTest {
     public GreenMailRule greenMail = new GreenMailRule();
 
     @Page
-    protected ChangePasswordPage changePasswordPage;
+    protected LoginPasswordResetPage resetPasswordPage;
+
+    @Page
+    protected LoginPasswordUpdatePage updatePasswordPage;
 
     @Test
     public void resetPassword() throws IOException, MessagingException {
@@ -61,12 +65,13 @@ public class ResetPasswordTest extends AbstractDroneTest {
 
         Assert.assertTrue(loginPage.isCurrent());
         
-        // TODO Replace with clicking reset password link when added
-        String url = browser.getCurrentUrl();
-        url = url.replace("tokens/login", "account/password-reset");
-        url = url + "&username=bburke@redhat.com";
+        loginPage.resetPassword();
+
+        Assert.assertTrue(resetPasswordPage.isCurrent());
 
-        browser.navigate().to(url);
+        resetPasswordPage.changePassword("bburke@redhat.com", "bburke@redhat.com");
+
+        Assert.assertTrue(resetPasswordPage.isCurrent());
 
         Assert.assertEquals(1, greenMail.getReceivedMessages().length);
 
@@ -77,38 +82,22 @@ public class ResetPasswordTest extends AbstractDroneTest {
 
         browser.navigate().to(changePasswordUrl.trim());
 
-        changePasswordPage.changePassword("new-password", "new-password");
+        Assert.assertTrue(updatePasswordPage.isCurrent());
 
-        Assert.assertTrue(loginPage.isCurrent());
-
-        loginPage.login("bburke@redhat.com", "password");
-        Assert.assertTrue(loginPage.isCurrent());
-        Assert.assertEquals("Invalid username or password", loginPage.getError());
-
-        loginPage.login("bburke@redhat.com", "new-password");
+        updatePasswordPage.changePassword("new-password", "new-password");
 
         Assert.assertTrue(appPage.isCurrent());
         Assert.assertEquals("bburke@redhat.com", appPage.getUser());
-    }
 
-    @Test
-    public void tempPassword() {
+        appPage.logout();
         appPage.open();
 
         Assert.assertTrue(loginPage.isCurrent());
 
-        loginPage.login("reset@pass.com", "temp-password");
-
-        Assert.assertTrue(changePasswordPage.isCurrent());
-
-        changePasswordPage.changePassword("new-password", "new-password");
-
-        Assert.assertTrue(loginPage.isCurrent());
-
-        loginPage.login("reset@pass.com", "new-password");
+        loginPage.login("bburke@redhat.com", "new-password");
 
         Assert.assertTrue(appPage.isCurrent());
-        Assert.assertEquals("reset@pass.com", appPage.getUser());
+        Assert.assertEquals("bburke@redhat.com", appPage.getUser());
     }
 
 }
diff --git a/testsuite/src/test/resources/testrealm.json b/testsuite/src/test/resources/testrealm.json
index 0ea2810..72a8d61 100755
--- a/testsuite/src/test/resources/testrealm.json
+++ b/testsuite/src/test/resources/testrealm.json
@@ -27,7 +27,7 @@
         {
             "username" : "reset@pass.com",
             "enabled": true,
-            "requiredActions" : [ "RESET_PASSWORD" ],
+            "requiredActions" : [ "UPDATE_PASSWORD" ],
             "email" : "reset@pass.com",
             "credentials" : [
                 { "type" : "password",
@@ -35,6 +35,16 @@
             ]
         },
         {
+            "username" : "updateprof@pass.com",
+            "enabled": true,
+            "requiredActions" : [ "UPDATE_PROFILE" ],
+            "email" : "reset@pass.com",
+            "credentials" : [
+                { "type" : "password",
+                    "value" : "password" }
+            ]
+        },
+        {
             "username" : "third-party",
             "enabled": true,
             "credentials" : [
@@ -63,6 +73,10 @@
             "roles": ["user"]
         },
         {
+            "username": "updateprof@pass.com",
+            "roles": ["user"]
+        },
+        {
             "username": "third-party",
             "roles": ["KEYCLOAK_IDENTITY_REQUESTER"]
         }