keycloak-memoizeit

Details

diff --git a/forms/src/main/java/org/keycloak/forms/ErrorBean.java b/forms/src/main/java/org/keycloak/forms/ErrorBean.java
index ff140bd..d6c18b6 100644
--- a/forms/src/main/java/org/keycloak/forms/ErrorBean.java
+++ b/forms/src/main/java/org/keycloak/forms/ErrorBean.java
@@ -21,6 +21,8 @@
  */
 package org.keycloak.forms;
 
+import org.keycloak.services.resources.flows.FormFlows;
+
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
@@ -28,12 +30,32 @@ public class ErrorBean {
 
     private String summary;
 
+    private FormFlows.ErrorType type;
+
+    // Message is considered ERROR by default
     public ErrorBean(String summary) {
+        this(summary, FormFlows.ErrorType.ERROR);
+    }
+
+    public ErrorBean(String summary, FormFlows.ErrorType type) {
         this.summary = summary;
+        this.type = type;
     }
 
     public String getSummary() {
         return summary;
     }
 
+    public boolean isSuccess(){
+        return FormFlows.ErrorType.SUCCESS.equals(this.type);
+    }
+
+    public boolean isWarning(){
+        return FormFlows.ErrorType.WARNING.equals(this.type);
+    }
+
+    public boolean isError(){
+        return FormFlows.ErrorType.ERROR.equals(this.type);
+    }
+
 }
\ No newline at end of file
diff --git a/forms/src/main/java/org/keycloak/service/FormServiceImpl.java b/forms/src/main/java/org/keycloak/service/FormServiceImpl.java
index 1df2436..7efa87d 100644
--- a/forms/src/main/java/org/keycloak/service/FormServiceImpl.java
+++ b/forms/src/main/java/org/keycloak/service/FormServiceImpl.java
@@ -42,6 +42,7 @@ import org.keycloak.forms.TotpBean;
 import org.keycloak.forms.UrlBean;
 import org.keycloak.forms.UserBean;
 import org.keycloak.services.FormService;
+import org.keycloak.services.resources.flows.FormFlows;
 import org.keycloak.services.resources.flows.Pages;
 
 /**
@@ -149,6 +150,10 @@ public class FormServiceImpl implements FormService {
 
     private class CommandPassword implements Command {
         public void exec(Map<String, Object> attributes, FormServiceDataBean dataBean) {
+            if (dataBean.getError() != null){
+                attributes.put("message", new ErrorBean(dataBean.getError(), dataBean.getErrorType()));
+            }
+
             RealmBean realm = new RealmBean(dataBean.getRealm());
 
             attributes.put("realm", realm);
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/forms.css b/forms/src/main/resources/META-INF/resources/forms/theme/default/css/forms.css
index 062012c..78a16e4 100644
--- a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/forms.css
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/css/forms.css
@@ -250,19 +250,19 @@ button.primary:enabled:active {
   background-position: 1.27272727272727em 1.63636363636364em;
 }
 .feedback.error {
-  background-image: url(img/feedback-error-arrow-down.svg);
+  background-image: url(img/feedback-error-arrow-down.png);
 }
 .feedback.error p {
   border-color: #b91415;
-  background-image: url(img/feedback-error-sign.svg);
+  background-image: url(img/feedback-error-sign.png);
   background-color: #f8e7e7;
 }
 .feedback.success {
-  background-image: url(img/feedback-success-arrow-down.svg);
+  background-image: url(img/feedback-success-arrow-down.png);
 }
 .feedback.success p {
   border-color: #4b9e39;
-  background-image: url(img/feedback-success-sign.svg);
+  background-image: url(img/feedback-success-sign.png);
   background-color: #e4f1e1;
 }
 .feedback.warning p {
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/feedback-success-arrow-down.png b/forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/feedback-success-arrow-down.png
new file mode 100644
index 0000000..03cc0c4
Binary files /dev/null and b/forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/feedback-success-arrow-down.png differ
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/feedback-success-sign.png b/forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/feedback-success-sign.png
new file mode 100644
index 0000000..640bd71
Binary files /dev/null and b/forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/feedback-success-sign.png differ
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/login-register.css b/forms/src/main/resources/META-INF/resources/forms/theme/default/css/login-register.css
index 073ff39..8908e52 100644
--- a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/login-register.css
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/css/login-register.css
@@ -324,6 +324,12 @@ a.zocial:before {
     line-height: 1.3em;
 }
 
+.rcue-login-register.reset p.subtitle {
+    margin-bottom: 10px;
+    position: inherit;
+    text-align: right;
+}
+
 .rcue-login-register .background-area p.instruction.instruction.second {
     color: #999999;
 }
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 6aaddd7..60ae59a 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
@@ -1,30 +1,36 @@
 <#import "template-login-action.ftl" as layout>
-<@layout.registrationLayout bodyClass=""; section>
+<@layout.registrationLayout bodyClass="reset"; section>
     <#if section = "title">
 
-    Reset password
+    ${rb.getString('emailForgotHeader')}
 
     <#elseif section = "header">
 
-    Reset password
+    ${rb.getString('emailForgotHeader')}
 
     <#elseif section = "form">
 
     <div id="form">
+        <#if message?has_content>
+            <#if message.success>
+                <div class="feedback success bottom-left show"><p><strong>${rb.getString('successHeader')}</strong> ${rb.getString(message.summary)}</p></div>
+            </#if>
+            <#if message.error>
+                <div class="feedback error bottom-left show"><p><strong>${rb.getString('errorHeader')}</strong><br/>${rb.getString(message.summary)}</p></div>
+            </#if>
+        </#if>
+
+        <p class="instruction">${rb.getString('emailInstruction')}</p>
         <form action="${url.passwordResetUrl}" method="post">
             <div>
-                <label for="username">${rb.getString('username')}</label>
-                <input id="username" name="username" type="text" />
+                <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" />
+      	    	<label for="email">${rb.getString('email')}</label><input type="text" id="email" name="email" />
 			</div>
-
             <input class="btn-primary" type="submit" value="Submit" />
         </form>
     </div>
-
     <#elseif section = "info" >
 
     <div id="info">
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 7a13ecc..a9b1ed2 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
@@ -1,24 +1,22 @@
 <#import "template-login-action.ftl" as layout>
-<@layout.registrationLayout bodyClass=""; section>
+<@layout.registrationLayout bodyClass="reset"; section>
     <#if section = "title">
 
-    Update password
+    ${rb.getString('emailUpdateHeader')}
 
     <#elseif section = "header">
 
-    Update password
+    ${rb.getString('emailUpdateHeader')}
 
     <#elseif section = "form">
 
     <div id="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" />
+            	<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" />
+        	    <label for="password-confirm">${rb.getString('passwordConfirm')}</label><input type="password" id="password-confirm" name="password-confirm" />
 	        </div>
 
             <input class="btn-primary" type="submit" value="Submit" />
diff --git a/forms/src/main/resources/org/keycloak/forms/messages.properties b/forms/src/main/resources/org/keycloak/forms/messages.properties
index ce6667a..92722f9 100644
--- a/forms/src/main/resources/org/keycloak/forms/messages.properties
+++ b/forms/src/main/resources/org/keycloak/forms/messages.properties
@@ -17,6 +17,7 @@ email=Email
 password=Password
 passwordConfirm=Confirm Password
 passwordNew=New Password
+passwordNewConfirm=New Password confirmation
 
 authenticatorCode=One-time-password
 clientCertificate=Client Certificate
@@ -39,3 +40,14 @@ invalidTotp=Invalid authenticator code
 usernameExists=Username already exists
 
 error=A system error has occured, contact admin
+
+successHeader=Success!
+errorHeader=Error!
+
+# Forgot password part
+
+emailForgotHeader=Forgot Your Password?
+emailUpdateHeader=Update password
+emailSent=You should receive an email shortly with further instructions.
+emailError=Invalid username or email.
+emailInstruction=Enter your username and email address and we will send you instructions on how to create a new password.
\ 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 cdf52d8..8bd181c 100644
--- a/services/src/main/java/org/keycloak/services/email/EmailSender.java
+++ b/services/src/main/java/org/keycloak/services/email/EmailSender.java
@@ -103,9 +103,16 @@ public class EmailSender {
         URI uri = builder.build(realm.getId());
 
         StringBuilder sb = new StringBuilder();
+
+        sb.append("Hi ").append(user.getFirstName()).append(",\n\n");
+        sb.append("Someone just requested to change your Keycloak account's password. ");
+        sb.append("If this was you, click on the link below to set a new password:\n");
         sb.append(uri.toString());
-        sb.append("\n");
-        sb.append("Expires in " + TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()));
+        sb.append("\n\nThis link will expire within ").append(TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()));
+        sb.append(" minutes.\n\n");
+        sb.append("If you don't want to reset your password, just ignore this message and nothing will be changed.\n\n");
+        sb.append("Thanks,\n");
+        sb.append("The Keycloak Team");
 
         try {
             send(user.getEmail(), "Reset password link", sb.toString());
diff --git a/services/src/main/java/org/keycloak/services/FormService.java b/services/src/main/java/org/keycloak/services/FormService.java
index 8ddf517..50a0959 100644
--- a/services/src/main/java/org/keycloak/services/FormService.java
+++ b/services/src/main/java/org/keycloak/services/FormService.java
@@ -27,6 +27,7 @@ import javax.ws.rs.core.MultivaluedMap;
 
 import org.keycloak.services.models.RealmModel;
 import org.keycloak.services.models.UserModel;
+import org.keycloak.services.resources.flows.FormFlows;
 
 /**
  * @author <a href="mailto:vrockai@redhat.com">Viliam Rockai</a>
@@ -42,6 +43,9 @@ public interface FormService {
         private RealmModel realm;
         private UserModel userModel;
         private String error;
+
+        private FormFlows.ErrorType errorType;
+
         private MultivaluedMap<String, String> formData;
         private URI baseURI;
 
@@ -121,5 +125,13 @@ public interface FormService {
         public void setUserModel(UserModel userModel) {
             this.userModel = userModel;
         }
+
+        public FormFlows.ErrorType getErrorType() {
+            return errorType;
+        }
+
+        public void setErrorType(FormFlows.ErrorType errorType) {
+            this.errorType = errorType;
+        }
     }
 }
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 d4b643c..94e4682 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -240,8 +240,7 @@ public class AccountService {
 
         UserModel user = realm.getUser(username);
         if (user == null || !email.equals(user.getEmail())) {
-            return Flows.forms(realm, request, uriInfo).setError("Invalid username or email")
-                    .forwardToAction(RequiredAction.UPDATE_PASSWORD);
+            return Flows.forms(realm, request, uriInfo).setError("emailError").forwardToPasswordReset();
         }
 
         Set<RequiredAction> requiredActions = new HashSet<RequiredAction>(user.getRequiredActions());
@@ -253,7 +252,8 @@ public class AccountService {
 
         new EmailSender().sendPasswordReset(user, realm, accessCode, uriInfo);
 
-        return Flows.forms(realm, request, uriInfo).forwardToPasswordReset();
+        return Flows.forms(realm, request, uriInfo).setError("emailSent").setErrorType(FormFlows.ErrorType.SUCCESS)
+                .forwardToPasswordReset();
     }
 
     @Path("email-verification")
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 0c106d0..aac8260 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
@@ -53,7 +53,12 @@ public class FormFlows {
     public static final String SOCIAL_REGISTRATION = "socialRegistration";
     public static final String CODE = "code";
 
+    // TODO refactor/rename "error" to "message" everywhere where it makes sense
     private String error;
+
+    public static enum ErrorType {SUCCESS, WARNING, ERROR};
+    private ErrorType errorType;
+
     private MultivaluedMap<String, String> formData;
 
     private RealmModel realm;
@@ -98,6 +103,7 @@ public class FormFlows {
     private Response forwardToForm(String template) {
 
         FormService.FormServiceDataBean formDataBean = new FormService.FormServiceDataBean(realm, userModel, formData, error);
+        formDataBean.setErrorType(errorType == null ? ErrorType.ERROR : errorType);
 
         // Getting URI needed by form processing service
         ResteasyUriInfo uriInfo = request.getUri();
@@ -172,6 +178,11 @@ public class FormFlows {
         return this;
     }
 
+    public FormFlows setErrorType(ErrorType errorType) {
+        this.errorType = errorType;
+        return this;
+    }
+
     public FormFlows setUser(UserModel userModel) {
         this.userModel = userModel;
         return this;
diff --git a/testsuite/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
index 8dd56e3..9b5ca0f 100644
--- a/testsuite/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
+++ b/testsuite/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
@@ -85,12 +85,14 @@ public class ResetPasswordTest {
 
         resetPasswordPage.assertCurrent();
 
+        Assert.assertEquals("Success!", resetPasswordPage.getMessage());
+
         Assert.assertEquals(1, greenMail.getReceivedMessages().length);
 
         MimeMessage message = greenMail.getReceivedMessages()[0];
 
         String body = (String) message.getContent();
-        String changePasswordUrl = body.split("\n")[0];
+        String changePasswordUrl = body.split("\n")[3];
 
         driver.navigate().to(changePasswordUrl.trim());
 
@@ -109,4 +111,34 @@ public class ResetPasswordTest {
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
     }
 
+    @Test
+    public void resetPasswordWrongUsername() throws IOException, MessagingException {
+        loginPage.open();
+        loginPage.resetPassword();
+
+        resetPasswordPage.assertCurrent();
+
+        resetPasswordPage.changePassword("invalid", "test-user@localhost");
+
+        resetPasswordPage.assertCurrent();
+
+        Assert.assertNotEquals("Success!", resetPasswordPage.getMessage());
+        Assert.assertEquals("Error!", resetPasswordPage.getMessage());
+    }
+
+    @Test
+    public void resetPasswordWrongEmail() throws IOException, MessagingException {
+        loginPage.open();
+        loginPage.resetPassword();
+
+        resetPasswordPage.assertCurrent();
+
+        resetPasswordPage.changePassword("test-user@localhost", "invalid");
+
+        resetPasswordPage.assertCurrent();
+
+        Assert.assertNotEquals("Success!", resetPasswordPage.getMessage());
+        Assert.assertEquals("Error!", resetPasswordPage.getMessage());
+    }
+
 }
diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java
index 94edee8..45fc30a 100644
--- a/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java
+++ b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java
@@ -38,6 +38,9 @@ public class LoginPasswordResetPage extends Page {
     @FindBy(css = "input[type=\"submit\"]")
     private WebElement submitButton;
 
+    @FindBy(css = ".feedback > p > strong")
+    private WebElement emailErrorMessage;
+
     public void changePassword(String username, String email) {
         usernameInput.sendKeys(username);
         emailInput.sendKeys(email);
@@ -46,11 +49,15 @@ public class LoginPasswordResetPage extends Page {
     }
 
     public boolean isCurrent() {
-        return driver.getTitle().equals("Reset password");
+        return driver.getTitle().equals("Forgot Your Password?");
     }
 
     public void open() {
         throw new UnsupportedOperationException();
     }
 
+    public String getMessage() {
+        return emailErrorMessage != null ? emailErrorMessage.getText() : null;
+    }
+
 }