KEYCLOAK-74 Adapting the Forget password forms to original design
This commit is contained in:
parent
625061002f
commit
144f5f9cfd
15 changed files with 145 additions and 27 deletions
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 678 B |
Binary file not shown.
After Width: | Height: | Size: 410 B |
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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.
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue