keycloak-uncached

Added separate service to process required login actions -

10/16/2013 3:49:11 PM

Details

diff --git a/forms/src/main/java/org/keycloak/forms/UrlBean.java b/forms/src/main/java/org/keycloak/forms/UrlBean.java
index cb82aaa..0fb83c0 100644
--- a/forms/src/main/java/org/keycloak/forms/UrlBean.java
+++ b/forms/src/main/java/org/keycloak/forms/UrlBean.java
@@ -108,8 +108,16 @@ public class UrlBean {
         }
     }
 
-    public String getPasswordResetUrl() {
-        return Urls.accountPasswordReset(baseURI, realm.getId()).toString();
+    public String getLoginUpdatePasswordUrl() {
+        return Urls.loginActionUpdatePassword(baseURI, realm.getId()).toString();
+    }
+
+    public String getLoginUpdateTotpUrl() {
+        return Urls.loginActionUpdateTotp(baseURI, realm.getId()).toString();
+    }
+
+    public String getLoginUpdateProfileUrl() {
+        return Urls.loginActionUpdateProfile(baseURI, realm.getId()).toString();
     }
 
     public String getSocialUrl() {
@@ -124,8 +132,12 @@ public class UrlBean {
         return Urls.accountTotpRemove(baseURI, realm.getId()).toString();
     }
 
-    public String getEmailVerificationUrl() {
-        return Urls.accountEmailVerification(baseURI, realm.getId()).toString();
+    public String getLoginPasswordResetUrl() {
+        return Urls.loginPasswordReset(baseURI, realm.getId()).toString();
+    }
+
+    public String getLoginEmailVerificationUrl() {
+        return Urls.loginActionEmailVerification(baseURI, realm.getId()).toString();
     }
 
 }
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 863118e..7e63046 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
@@ -24,7 +24,7 @@
             </#list>
 
             <div class="aside-btn">
-                <p>Forgot <a href="${url.passwordResetUrl}">Password</a>?</p>
+                <p>Forgot <a href="${url.loginPasswordResetUrl}">Password</a>?</p>
             </div>
 
             <input class="btn-primary" type="submit" value="Log In"/>
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
index 9b3cca5..2b1c1c5 100755
--- 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
@@ -27,7 +27,7 @@
             </li>
             <li class="clearfix">
                 <p><strong>3</strong>Enter the one-time-password provided by Google Authenticator below and click Submit to finish the setup.</p>
-                <form action="${url.totpUrl}" method="post">
+                <form action="${url.loginUpdateTotpUrl}" method="post">
                     <div>
                         <label for="otp" class="two-lines">One-time-password</label><input type="text" id="totp" name="totp" />
                         <input type="hidden" id="totpSecret" name="totpSecret" value="${totp.totpSecret}" />
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
index 60ae59a..ad80199 100755
--- 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
@@ -21,7 +21,7 @@
         </#if>
 
         <p class="instruction">${rb.getString('emailInstruction')}</p>
-        <form action="${url.passwordResetUrl}" method="post">
+        <form action="${url.loginPasswordResetUrl}" method="post">
             <div>
                 <label for="username">${rb.getString('username')}</label><input id="username" name="username" type="text" />
             </div>
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
index a9b1ed2..4db1911 100755
--- 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
@@ -11,7 +11,7 @@
     <#elseif section = "form">
 
     <div id="form">
-        <form action="${url.passwordUrl}" method="post">
+        <form action="${url.loginUpdatePasswordUrl}" method="post">
         	<div>
             	<label for="password-new">${rb.getString('passwordNew')}</label><input type="password" id="password-new" name="password-new" />
         	</div>
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
index fb9a8e4..2683d2f 100755
--- 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
@@ -15,7 +15,7 @@
     <#elseif section = "form">
 
     <div id="form">
-        <form action="${url.accountUrl}" method="post">
+        <form action="${url.loginUpdateProfileUrl}" method="post">
             <div class="feedback error bottom-left">
                 <p><strong>Some required fields are empty or incorrect.</strong><br>Please correct the fields in red.</p>
             </div>
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
index d92c349..cff75f1 100755
--- 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
@@ -20,7 +20,7 @@
             Your account is not enabled. An email with instructions to verify your email address has been sent to you.
         </p>
         <p class="instruction">Haven't received a verification code in your email?
-            <a href="${url.emailVerificationUrl}">Click here</a> to re-send the email.
+            <a href="${url.loginEmailVerificationUrl}">Click here</a> to re-send the email.
         </p>
     </div>
 
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 93f59f9..6fd0907 100755
--- a/services/src/main/java/org/keycloak/services/email/EmailSender.java
+++ b/services/src/main/java/org/keycloak/services/email/EmailSender.java
@@ -79,7 +79,7 @@ public class EmailSender {
     }
 
     public void sendEmailVerification(UserModel user, RealmModel realm, AccessCodeEntry accessCode, UriInfo uriInfo) {
-        UriBuilder builder = Urls.accountBase(uriInfo.getBaseUri()).path(AccountService.class, "emailVerification");
+        UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri());
         builder.queryParam("key", accessCode.getId());
 
         URI uri = builder.build(realm.getId());
@@ -103,7 +103,7 @@ public class EmailSender {
     }
 
     public void sendPasswordReset(UserModel user, RealmModel realm, AccessCodeEntry accessCode, UriInfo uriInfo) {
-        UriBuilder builder = Urls.accountBase(uriInfo.getBaseUri()).path(AccountService.class, "passwordPage");
+        UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri());
         builder.queryParam("key", accessCode.getId());
 
         URI uri = builder.build(realm.getId());
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 2874971..7f54e52 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -98,8 +98,7 @@ public class AccountService {
     @POST
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
     public Response processAccountUpdate(final MultivaluedMap<String, String> formData) {
-        AccessCodeEntry accessCodeEntry = getAccessCodeEntry(RequiredAction.UPDATE_PROFILE);
-        UserModel user = accessCodeEntry != null ? getUserFromAccessCode(accessCodeEntry) : getUserFromAuthManager();
+        UserModel user = getUserFromAuthManager();
         if (user == null) {
             return Response.status(Status.FORBIDDEN).build();
         }
@@ -108,60 +107,7 @@ public class AccountService {
         user.setLastName(formData.getFirst("lastName"));
         user.setEmail(formData.getFirst("email"));
 
-        user.removeRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE);
-        if (accessCodeEntry != null) {
-            accessCodeEntry.getRequiredActions().remove(UserModel.RequiredAction.UPDATE_PROFILE);
-        }
-
-        if (accessCodeEntry != null) {
-            return redirectOauth(user, accessCodeEntry);
-        } else {
-            return Flows.forms(realm, request, uriInfo).setUser(user).forwardToAccount();
-        }
-    }
-
-    private UserModel getUserFromAccessCode(AccessCodeEntry accessCodeEntry) {
-        String loginName = accessCodeEntry.getUser().getLoginName();
-        return realm.getUser(loginName);
-    }
-
-    private UserModel getUserFromAuthManager() {
-        return authManager.authenticateIdentityCookie(realm, uriInfo, headers);
-    }
-
-    private AccessCodeEntry getAccessCodeEntry(RequiredAction requiredAction) {
-        String code = uriInfo.getQueryParameters().getFirst(FormFlows.CODE);
-        if (code == null) {
-            return null;
-        }
-
-        JWSInput input = new JWSInput(code, providers);
-        boolean verifiedCode = false;
-        try {
-            verifiedCode = RSAProvider.verify(input, realm.getPublicKey());
-        } catch (Exception ignored) {
-            return null;
-        }
-
-        if (!verifiedCode) {
-            return null;
-        }
-
-        String key = input.readContent(String.class);
-        AccessCodeEntry accessCodeEntry = tokenManager.getAccessCode(key);
-        if (accessCodeEntry == null) {
-            return null;
-        }
-
-        if (accessCodeEntry.isExpired()) {
-            return null;
-        }
-
-        if (accessCodeEntry.getRequiredActions() == null || !accessCodeEntry.getRequiredActions().contains(requiredAction)) {
-            return null;
-        }
-
-        return accessCodeEntry;
+        return Flows.forms(realm, request, uriInfo).setUser(user).forwardToAccount();
     }
 
     @Path("totp-remove")
@@ -177,8 +123,7 @@ public class AccountService {
     @POST
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
     public Response processTotpUpdate(final MultivaluedMap<String, String> formData) {
-        AccessCodeEntry accessCodeEntry = getAccessCodeEntry(RequiredAction.CONFIGURE_TOTP);
-        UserModel user = accessCodeEntry != null ? getUserFromAccessCode(accessCodeEntry) : getUserFromAuthManager();
+        UserModel user = getUserFromAuthManager();
         if (user == null) {
             return Response.status(Status.FORBIDDEN).build();
         }
@@ -205,150 +150,39 @@ public class AccountService {
         credentials.setValue(formData.getFirst("totpSecret"));
         realm.updateCredential(user, credentials);
 
-        user.removeRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
-        if (accessCodeEntry != null) {
-            accessCodeEntry.getRequiredActions().remove(UserModel.RequiredAction.CONFIGURE_TOTP);
-        }
-
         user.setTotp(true);
 
-        if (accessCodeEntry != null) {
-            return redirectOauth(user, accessCodeEntry);
-        } else {
-            return Flows.forms(realm, request, uriInfo).setError("successTotp").setErrorType(FormFlows.ErrorType.SUCCESS)
-                    .setUser(user).forwardToTotp();
-        }
-    }
-
-    @Path("password-reset")
-    @GET
-    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.");
-        }
-
-        UserModel user = realm.getUser(username);
-        if (user == null || !email.equals(user.getEmail())) {
-            return Flows.forms(realm, request, uriInfo).setError("emailError").forwardToPasswordReset();
-        }
-
-        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).setError("emailSent").setErrorType(FormFlows.ErrorType.SUCCESS)
-                .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 {
-            AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.VERIFY_EMAIL);
-            UserModel user = accessCode != null ? getUserFromAccessCode(accessCode) : null;
-            if (user == null) {
-                return Response.status(Status.FORBIDDEN).build();
-            }
-
-            return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(user)
-                    .forwardToAction(RequiredAction.VERIFY_EMAIL);
-        }
-    }
-
-    private Response redirectOauth(UserModel user, AccessCodeEntry accessCode) {
-        if (accessCode == null) {
-            return null;
-        }
-
-        Set<RequiredAction> requiredActions = user.getRequiredActions();
-        if (!requiredActions.isEmpty()) {
-            return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(user)
-                    .forwardToAction(requiredActions.iterator().next());
-        } else {
-            accessCode.setExpiration((System.currentTimeMillis() / 1000) + realm.getAccessCodeLifespan());
-            return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode,
-                    accessCode.getState(), accessCode.getRedirectUri());
-        }
+        return Flows.forms(realm, request, uriInfo).setError("successTotp").setErrorType(FormFlows.ErrorType.SUCCESS)
+                .setUser(user).forwardToTotp();
     }
 
     @Path("password")
     @POST
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
     public Response processPasswordUpdate(final MultivaluedMap<String, String> formData) {
-        AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.UPDATE_PASSWORD);
-        UserModel user = accessCode != null ? getUserFromAccessCode(accessCode) : getUserFromAuthManager();
+        UserModel user = 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");
         String passwordNew = formData.getFirst("password-new");
         String passwordConfirm = formData.getFirst("password-confirm");
 
-        String error = null;
-
         if (Validation.isEmpty(passwordNew)) {
-            error = Messages.MISSING_PASSWORD;
+            forms.setError(Messages.MISSING_PASSWORD).forwardToPassword();
         } else if (!passwordNew.equals(passwordConfirm)) {
-            error = Messages.INVALID_PASSWORD_CONFIRM;
+            forms.setError(Messages.INVALID_PASSWORD_CONFIRM).forwardToPassword();
         }
 
-        if (!loginAction) {
-            if (Validation.isEmpty(password)) {
-                error = Messages.MISSING_PASSWORD;
-            } else if (!realm.validatePassword(user, password)) {
-                error = Messages.INVALID_PASSWORD_EXISTING;
-            }
+        if (Validation.isEmpty(password)) {
+            forms.setError(Messages.MISSING_PASSWORD).forwardToPassword();
+        } else if (!realm.validatePassword(user, password)) {
+            forms.setError(Messages.INVALID_PASSWORD_EXISTING).forwardToPassword();
         }
 
-        if (error != null) {
-            return forms.setError(error).forwardToPassword();
-        }
 
         UserCredentialModel credentials = new UserCredentialModel();
         credentials.setType(CredentialRepresentation.PASSWORD);
@@ -356,16 +190,7 @@ public class AccountService {
 
         realm.updateCredential(user, credentials);
 
-        user.removeRequiredAction(RequiredAction.UPDATE_PASSWORD);
-        if (accessCode != null) {
-            accessCode.getRequiredActions().remove(UserModel.RequiredAction.UPDATE_PASSWORD);
-        }
-
-        if (accessCode != null) {
-            return redirectOauth(user, accessCode);
-        } else {
-            return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword();
-        }
+        return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword();
     }
 
     @Path("")
@@ -404,21 +229,14 @@ public class AccountService {
     @Path("password")
     @GET
     public Response passwordPage() {
-        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();
-            }
-
-            return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode)
-                    .forwardToAction(RequiredAction.UPDATE_PASSWORD);
-        } else {
-            UserModel user = getUserFromAuthManager();
-            if (user == null) {
-                return Response.status(Status.FORBIDDEN).build();
-            }
-            return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword();
+        UserModel user = getUserFromAuthManager();
+        if (user == null) {
+            return Response.status(Status.FORBIDDEN).build();
         }
+        return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword();
+    }
+
+    private UserModel getUserFromAuthManager() {
+        return authManager.authenticateIdentityCookie(realm, uriInfo, headers);
     }
 }
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 b6a86a2..200b746 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
@@ -35,7 +35,7 @@ public class Urls {
         return accountBase(baseUri).path(AccountService.class, "accessPage").build(realmId);
     }
 
-    public static UriBuilder accountBase(URI baseUri) {
+    private static UriBuilder accountBase(URI baseUri) {
         return realmBase(baseUri).path(RealmsResource.class, "getAccountService");
     }
 
@@ -59,12 +59,32 @@ public class Urls {
         return accountBase(baseUri).path(AccountService.class, "processTotpRemove").build(realmId);
     }
 
-    public static URI accountEmailVerification(URI baseUri, String realmId) {
-        return accountBase(baseUri).path(AccountService.class, "emailVerification").build(realmId);
+    public static URI loginActionUpdatePassword(URI baseUri, String realmId) {
+        return requiredActionsBase(baseUri).path(RequiredActionsService.class, "updatePassword").build(realmId);
     }
 
-    public static URI accountPasswordReset(URI baseUri, String realmId) {
-        return accountBase(baseUri).path(AccountService.class, "passwordReset").build(realmId);
+    public static URI loginActionUpdateTotp(URI baseUri, String realmId) {
+        return requiredActionsBase(baseUri).path(RequiredActionsService.class, "updateTotp").build(realmId);
+    }
+
+    public static URI loginActionUpdateProfile(URI baseUri, String realmId) {
+        return requiredActionsBase(baseUri).path(RequiredActionsService.class, "updateProfile").build(realmId);
+    }
+
+    public static URI loginActionEmailVerification(URI baseUri, String realmId) {
+        return loginActionEmailVerificationBuilder(baseUri).build(realmId);
+    }
+
+    public static UriBuilder loginActionEmailVerificationBuilder(URI baseUri) {
+        return requiredActionsBase(baseUri).path(RequiredActionsService.class, "emailVerification");
+    }
+
+    public static URI loginPasswordReset(URI baseUri, String realmId) {
+        return loginPasswordResetBuilder(baseUri).build(realmId);
+    }
+
+    public static UriBuilder loginPasswordResetBuilder(URI baseUri) {
+        return requiredActionsBase(baseUri).path(RequiredActionsService.class, "passwordReset");
     }
 
     private static UriBuilder realmBase(URI baseUri) {
@@ -120,11 +140,15 @@ public class Urls {
                 .build(realmId);
     }
 
-    private static UriBuilder tokenBase(URI baseUri) {
-        return realmBase(baseUri).path(RealmsResource.class, "getTokenService");
-    }
-
     public static URI socialRegisterAction(URI baseUri, String realmId) {
         return socialBase(baseUri).path(SocialResource.class, "socialRegistration").build(realmId);
     }
+
+    private static UriBuilder requiredActionsBase(URI baseUri) {
+        return tokenBase(baseUri).path(TokenService.class, "getRequiredActionsService");
+    }
+
+    private static UriBuilder tokenBase(URI baseUri) {
+        return realmBase(baseUri).path(RealmsResource.class, "getTokenService");
+    }
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
index ee344b8..214a814 100755
--- a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
@@ -29,6 +29,7 @@ import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserModel.RequiredAction;
 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.TokenManager;
@@ -43,8 +44,8 @@ import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.core.*;
-import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.ext.Providers;
+import java.util.HashSet;
 import java.util.Set;
 
 /**
@@ -75,7 +76,7 @@ public class RequiredActionsService {
         this.tokenManager = tokenManager;
     }
 
-    @Path("")
+    @Path("profile")
     @POST
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
     public Response updateProfile(final MultivaluedMap<String, String> formData) {
@@ -98,7 +99,7 @@ public class RequiredActionsService {
     @Path("totp")
     @POST
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
-    public Response configureTotp(final MultivaluedMap<String, String> formData) {
+    public Response updateTotp(final MultivaluedMap<String, String> formData) {
         AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.CONFIGURE_TOTP);
         if (accessCode == null) {
             return forwardToErrorPage();
@@ -129,6 +130,46 @@ public class RequiredActionsService {
         return redirectOauth(user, accessCode);
     }
 
+    @Path("password")
+    @POST
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response updatePassword(final MultivaluedMap<String, String> formData) {
+        AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.UPDATE_PASSWORD);
+        if (accessCode == null) {
+            return forwardToErrorPage();
+        }
+
+        UserModel user = getUser(accessCode);
+
+        String password = formData.getFirst("password");
+        String passwordNew = formData.getFirst("password-new");
+        String passwordConfirm = formData.getFirst("password-confirm");
+
+        FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user);
+        if (Validation.isEmpty(passwordNew)) {
+            forms.setError(Messages.MISSING_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD);
+        } else if (!passwordNew.equals(passwordConfirm)) {
+            forms.setError(Messages.MISSING_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD);
+        }
+
+        UserCredentialModel credentials = new UserCredentialModel();
+        credentials.setType(CredentialRepresentation.PASSWORD);
+        credentials.setValue(passwordNew);
+
+        realm.updateCredential(user, credentials);
+
+        user.removeRequiredAction(RequiredAction.UPDATE_PASSWORD);
+        if (accessCode != null) {
+            accessCode.getRequiredActions().remove(RequiredAction.UPDATE_PASSWORD);
+        }
+
+        if (accessCode != null) {
+            return redirectOauth(user, accessCode);
+        } else {
+            return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword();
+        }
+    }
+
 
     @Path("email-verification")
     @GET
@@ -158,44 +199,59 @@ public class RequiredActionsService {
         }
     }
 
-    @Path("password")
+    @Path("password-reset")
+    @GET
+    public Response passwordReset() {
+        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 forwardToErrorPage();
+            }
+            return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).forwardToAction(RequiredAction.UPDATE_PASSWORD);
+        } else {
+            return Flows.forms(realm, request, uriInfo).forwardToPasswordReset();
+        }
+    }
+
+    @Path("password-reset")
     @POST
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
-    public Response resetPassword(final MultivaluedMap<String, String> formData) {
-        AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.UPDATE_PASSWORD);
-        if (accessCode == null) {
-            return forwardToErrorPage();
+    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.");
         }
 
-        UserModel user = getUser(accessCode);
-
-        String password = formData.getFirst("password");
-        String passwordNew = formData.getFirst("password-new");
-        String passwordConfirm = formData.getFirst("password-confirm");
-
-        FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user);
-        if (Validation.isEmpty(passwordNew)) {
-            forms.setError(Messages.MISSING_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD);
-        } else if (!passwordNew.equals(passwordConfirm)) {
-            forms.setError(Messages.MISSING_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD);
+        UserModel user = realm.getUser(username);
+        if (user == null || !email.equals(user.getEmail())) {
+            return Flows.forms(realm, request, uriInfo).setError("emailError").forwardToPasswordReset();
         }
 
-        UserCredentialModel credentials = new UserCredentialModel();
-        credentials.setType(CredentialRepresentation.PASSWORD);
-        credentials.setValue(passwordNew);
+        Set<RequiredAction> requiredActions = new HashSet<RequiredAction>(user.getRequiredActions());
+        requiredActions.add(RequiredAction.UPDATE_PASSWORD);
 
-        realm.updateCredential(user, credentials);
+        AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user);
+        accessCode.setRequiredActions(requiredActions);
+        accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction());
 
-        user.removeRequiredAction(RequiredAction.UPDATE_PASSWORD);
-        if (accessCode != null) {
-            accessCode.getRequiredActions().remove(RequiredAction.UPDATE_PASSWORD);
-        }
+        new EmailSender().sendPasswordReset(user, realm, accessCode, uriInfo);
 
-        if (accessCode != null) {
-            return redirectOauth(user, accessCode);
-        } else {
-            return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword();
-        }
+        return Flows.forms(realm, request, uriInfo).setError("emailSent").setErrorType(FormFlows.ErrorType.SUCCESS)
+                .forwardToPasswordReset();
     }
 
     private AccessCodeEntry getAccessCodeEntry(RequiredAction requiredAction) {
diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java
index c8bfd0e..d72e80d 100755
--- a/services/src/main/java/org/keycloak/services/resources/TokenService.java
+++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java
@@ -30,6 +30,7 @@ import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
+import javax.ws.rs.container.ResourceContext;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
@@ -75,6 +76,8 @@ public class TokenService {
     @Context
     protected KeycloakTransaction transaction;
 
+    @Context
+    protected ResourceContext resourceContext;
 
     private ResourceAdminManager resourceAdminManager = new ResourceAdminManager();
 
@@ -219,7 +222,9 @@ public class TokenService {
 
     @Path("auth/request/login-actions")
     public RequiredActionsService getRequiredActionsService() {
-        return new RequiredActionsService(realm, tokenManager);
+        RequiredActionsService service = new RequiredActionsService(realm, tokenManager);
+        resourceContext.initResource(service);
+        return service;
     }
 
     private void isTotpConfigurationRequired(UserModel user) {