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;
|
package org.keycloak.forms;
|
||||||
|
|
||||||
|
import org.keycloak.services.resources.flows.FormFlows;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
|
@ -28,12 +30,32 @@ public class ErrorBean {
|
||||||
|
|
||||||
private String summary;
|
private String summary;
|
||||||
|
|
||||||
|
private FormFlows.ErrorType type;
|
||||||
|
|
||||||
|
// Message is considered ERROR by default
|
||||||
public ErrorBean(String summary) {
|
public ErrorBean(String summary) {
|
||||||
|
this(summary, FormFlows.ErrorType.ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ErrorBean(String summary, FormFlows.ErrorType type) {
|
||||||
this.summary = summary;
|
this.summary = summary;
|
||||||
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSummary() {
|
public String getSummary() {
|
||||||
return summary;
|
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.UrlBean;
|
||||||
import org.keycloak.forms.UserBean;
|
import org.keycloak.forms.UserBean;
|
||||||
import org.keycloak.services.FormService;
|
import org.keycloak.services.FormService;
|
||||||
|
import org.keycloak.services.resources.flows.FormFlows;
|
||||||
import org.keycloak.services.resources.flows.Pages;
|
import org.keycloak.services.resources.flows.Pages;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -149,6 +150,10 @@ public class FormServiceImpl implements FormService {
|
||||||
|
|
||||||
private class CommandPassword implements Command {
|
private class CommandPassword implements Command {
|
||||||
public void exec(Map<String, Object> attributes, FormServiceDataBean dataBean) {
|
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());
|
RealmBean realm = new RealmBean(dataBean.getRealm());
|
||||||
|
|
||||||
attributes.put("realm", realm);
|
attributes.put("realm", realm);
|
||||||
|
|
|
@ -250,19 +250,19 @@ button.primary:enabled:active {
|
||||||
background-position: 1.27272727272727em 1.63636363636364em;
|
background-position: 1.27272727272727em 1.63636363636364em;
|
||||||
}
|
}
|
||||||
.feedback.error {
|
.feedback.error {
|
||||||
background-image: url(img/feedback-error-arrow-down.svg);
|
background-image: url(img/feedback-error-arrow-down.png);
|
||||||
}
|
}
|
||||||
.feedback.error p {
|
.feedback.error p {
|
||||||
border-color: #b91415;
|
border-color: #b91415;
|
||||||
background-image: url(img/feedback-error-sign.svg);
|
background-image: url(img/feedback-error-sign.png);
|
||||||
background-color: #f8e7e7;
|
background-color: #f8e7e7;
|
||||||
}
|
}
|
||||||
.feedback.success {
|
.feedback.success {
|
||||||
background-image: url(img/feedback-success-arrow-down.svg);
|
background-image: url(img/feedback-success-arrow-down.png);
|
||||||
}
|
}
|
||||||
.feedback.success p {
|
.feedback.success p {
|
||||||
border-color: #4b9e39;
|
border-color: #4b9e39;
|
||||||
background-image: url(img/feedback-success-sign.svg);
|
background-image: url(img/feedback-success-sign.png);
|
||||||
background-color: #e4f1e1;
|
background-color: #e4f1e1;
|
||||||
}
|
}
|
||||||
.feedback.warning p {
|
.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;
|
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 {
|
.rcue-login-register .background-area p.instruction.instruction.second {
|
||||||
color: #999999;
|
color: #999999;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,36 @@
|
||||||
<#import "template-login-action.ftl" as layout>
|
<#import "template-login-action.ftl" as layout>
|
||||||
<@layout.registrationLayout bodyClass=""; section>
|
<@layout.registrationLayout bodyClass="reset"; section>
|
||||||
<#if section = "title">
|
<#if section = "title">
|
||||||
|
|
||||||
Reset password
|
${rb.getString('emailForgotHeader')}
|
||||||
|
|
||||||
<#elseif section = "header">
|
<#elseif section = "header">
|
||||||
|
|
||||||
Reset password
|
${rb.getString('emailForgotHeader')}
|
||||||
|
|
||||||
<#elseif section = "form">
|
<#elseif section = "form">
|
||||||
|
|
||||||
<div id="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">
|
<form action="${url.passwordResetUrl}" method="post">
|
||||||
<div>
|
<div>
|
||||||
<label for="username">${rb.getString('username')}</label>
|
<label for="username">${rb.getString('username')}</label><input id="username" name="username" type="text" />
|
||||||
<input id="username" name="username" type="text" />
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="email">${rb.getString('email')}</label>
|
<label for="email">${rb.getString('email')}</label><input type="text" id="email" name="email" />
|
||||||
<input type="text" id="email" name="email" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input class="btn-primary" type="submit" value="Submit" />
|
<input class="btn-primary" type="submit" value="Submit" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<#elseif section = "info" >
|
<#elseif section = "info" >
|
||||||
|
|
||||||
<div id="info">
|
<div id="info">
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
<#import "template-login-action.ftl" as layout>
|
<#import "template-login-action.ftl" as layout>
|
||||||
<@layout.registrationLayout bodyClass=""; section>
|
<@layout.registrationLayout bodyClass="reset"; section>
|
||||||
<#if section = "title">
|
<#if section = "title">
|
||||||
|
|
||||||
Update password
|
${rb.getString('emailUpdateHeader')}
|
||||||
|
|
||||||
<#elseif section = "header">
|
<#elseif section = "header">
|
||||||
|
|
||||||
Update password
|
${rb.getString('emailUpdateHeader')}
|
||||||
|
|
||||||
<#elseif section = "form">
|
<#elseif section = "form">
|
||||||
|
|
||||||
<div id="form">
|
<div id="form">
|
||||||
<form action="${url.passwordUrl}" method="post">
|
<form action="${url.passwordUrl}" method="post">
|
||||||
<div>
|
<div>
|
||||||
<label for="password-new">${rb.getString('passwordNew')}</label>
|
<label for="password-new">${rb.getString('passwordNew')}</label><input type="password" id="password-new" name="password-new" />
|
||||||
<input type="password" id="password-new" name="password-new" />
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="password-confirm">${rb.getString('passwordConfirm')}</label>
|
<label for="password-confirm">${rb.getString('passwordConfirm')}</label><input type="password" id="password-confirm" name="password-confirm" />
|
||||||
<input type="password" id="password-confirm" name="password-confirm" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input class="btn-primary" type="submit" value="Submit" />
|
<input class="btn-primary" type="submit" value="Submit" />
|
||||||
|
|
|
@ -17,6 +17,7 @@ email=Email
|
||||||
password=Password
|
password=Password
|
||||||
passwordConfirm=Confirm Password
|
passwordConfirm=Confirm Password
|
||||||
passwordNew=New Password
|
passwordNew=New Password
|
||||||
|
passwordNewConfirm=New Password confirmation
|
||||||
|
|
||||||
authenticatorCode=One-time-password
|
authenticatorCode=One-time-password
|
||||||
clientCertificate=Client Certificate
|
clientCertificate=Client Certificate
|
||||||
|
@ -39,3 +40,14 @@ invalidTotp=Invalid authenticator code
|
||||||
usernameExists=Username already exists
|
usernameExists=Username already exists
|
||||||
|
|
||||||
error=A system error has occured, contact admin
|
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.RealmModel;
|
||||||
import org.keycloak.services.models.UserModel;
|
import org.keycloak.services.models.UserModel;
|
||||||
|
import org.keycloak.services.resources.flows.FormFlows;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:vrockai@redhat.com">Viliam Rockai</a>
|
* @author <a href="mailto:vrockai@redhat.com">Viliam Rockai</a>
|
||||||
|
@ -42,6 +43,9 @@ public interface FormService {
|
||||||
private RealmModel realm;
|
private RealmModel realm;
|
||||||
private UserModel userModel;
|
private UserModel userModel;
|
||||||
private String error;
|
private String error;
|
||||||
|
|
||||||
|
private FormFlows.ErrorType errorType;
|
||||||
|
|
||||||
private MultivaluedMap<String, String> formData;
|
private MultivaluedMap<String, String> formData;
|
||||||
private URI baseURI;
|
private URI baseURI;
|
||||||
|
|
||||||
|
@ -121,5 +125,13 @@ public interface FormService {
|
||||||
public void setUserModel(UserModel userModel) {
|
public void setUserModel(UserModel userModel) {
|
||||||
this.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());
|
URI uri = builder.build(realm.getId());
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
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(uri.toString());
|
||||||
sb.append("\n");
|
sb.append("\n\nThis link will expire within ").append(TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()));
|
||||||
sb.append("Expires in " + 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 {
|
try {
|
||||||
send(user.getEmail(), "Reset password link", sb.toString());
|
send(user.getEmail(), "Reset password link", sb.toString());
|
||||||
|
|
|
@ -240,8 +240,7 @@ public class AccountService {
|
||||||
|
|
||||||
UserModel user = realm.getUser(username);
|
UserModel user = realm.getUser(username);
|
||||||
if (user == null || !email.equals(user.getEmail())) {
|
if (user == null || !email.equals(user.getEmail())) {
|
||||||
return Flows.forms(realm, request, uriInfo).setError("Invalid username or email")
|
return Flows.forms(realm, request, uriInfo).setError("emailError").forwardToPasswordReset();
|
||||||
.forwardToAction(RequiredAction.UPDATE_PASSWORD);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<RequiredAction> requiredActions = new HashSet<RequiredAction>(user.getRequiredActions());
|
Set<RequiredAction> requiredActions = new HashSet<RequiredAction>(user.getRequiredActions());
|
||||||
|
@ -253,7 +252,8 @@ public class AccountService {
|
||||||
|
|
||||||
new EmailSender().sendPasswordReset(user, realm, accessCode, uriInfo);
|
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")
|
@Path("email-verification")
|
||||||
|
|
|
@ -53,7 +53,12 @@ public class FormFlows {
|
||||||
public static final String SOCIAL_REGISTRATION = "socialRegistration";
|
public static final String SOCIAL_REGISTRATION = "socialRegistration";
|
||||||
public static final String CODE = "code";
|
public static final String CODE = "code";
|
||||||
|
|
||||||
|
// TODO refactor/rename "error" to "message" everywhere where it makes sense
|
||||||
private String error;
|
private String error;
|
||||||
|
|
||||||
|
public static enum ErrorType {SUCCESS, WARNING, ERROR};
|
||||||
|
private ErrorType errorType;
|
||||||
|
|
||||||
private MultivaluedMap<String, String> formData;
|
private MultivaluedMap<String, String> formData;
|
||||||
|
|
||||||
private RealmModel realm;
|
private RealmModel realm;
|
||||||
|
@ -98,6 +103,7 @@ public class FormFlows {
|
||||||
private Response forwardToForm(String template) {
|
private Response forwardToForm(String template) {
|
||||||
|
|
||||||
FormService.FormServiceDataBean formDataBean = new FormService.FormServiceDataBean(realm, userModel, formData, error);
|
FormService.FormServiceDataBean formDataBean = new FormService.FormServiceDataBean(realm, userModel, formData, error);
|
||||||
|
formDataBean.setErrorType(errorType == null ? ErrorType.ERROR : errorType);
|
||||||
|
|
||||||
// Getting URI needed by form processing service
|
// Getting URI needed by form processing service
|
||||||
ResteasyUriInfo uriInfo = request.getUri();
|
ResteasyUriInfo uriInfo = request.getUri();
|
||||||
|
@ -172,6 +178,11 @@ public class FormFlows {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FormFlows setErrorType(ErrorType errorType) {
|
||||||
|
this.errorType = errorType;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public FormFlows setUser(UserModel userModel) {
|
public FormFlows setUser(UserModel userModel) {
|
||||||
this.userModel = userModel;
|
this.userModel = userModel;
|
||||||
return this;
|
return this;
|
||||||
|
|
|
@ -85,12 +85,14 @@ public class ResetPasswordTest {
|
||||||
|
|
||||||
resetPasswordPage.assertCurrent();
|
resetPasswordPage.assertCurrent();
|
||||||
|
|
||||||
|
Assert.assertEquals("Success!", resetPasswordPage.getMessage());
|
||||||
|
|
||||||
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
|
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
|
||||||
|
|
||||||
MimeMessage message = greenMail.getReceivedMessages()[0];
|
MimeMessage message = greenMail.getReceivedMessages()[0];
|
||||||
|
|
||||||
String body = (String) message.getContent();
|
String body = (String) message.getContent();
|
||||||
String changePasswordUrl = body.split("\n")[0];
|
String changePasswordUrl = body.split("\n")[3];
|
||||||
|
|
||||||
driver.navigate().to(changePasswordUrl.trim());
|
driver.navigate().to(changePasswordUrl.trim());
|
||||||
|
|
||||||
|
@ -109,4 +111,34 @@ public class ResetPasswordTest {
|
||||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
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\"]")
|
@FindBy(css = "input[type=\"submit\"]")
|
||||||
private WebElement submitButton;
|
private WebElement submitButton;
|
||||||
|
|
||||||
|
@FindBy(css = ".feedback > p > strong")
|
||||||
|
private WebElement emailErrorMessage;
|
||||||
|
|
||||||
public void changePassword(String username, String email) {
|
public void changePassword(String username, String email) {
|
||||||
usernameInput.sendKeys(username);
|
usernameInput.sendKeys(username);
|
||||||
emailInput.sendKeys(email);
|
emailInput.sendKeys(email);
|
||||||
|
@ -46,11 +49,15 @@ public class LoginPasswordResetPage extends Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isCurrent() {
|
public boolean isCurrent() {
|
||||||
return driver.getTitle().equals("Reset password");
|
return driver.getTitle().equals("Forgot Your Password?");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void open() {
|
public void open() {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return emailErrorMessage != null ? emailErrorMessage.getText() : null;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue