diff --git a/forms/common-themes/src/main/resources/theme/login/base/login-reset-password.ftl b/forms/common-themes/src/main/resources/theme/login/base/login-reset-password.ftl index a2042c2b50..aca7d76ac8 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/login-reset-password.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login-reset-password.ftl @@ -8,10 +8,10 @@
- +
- +
diff --git a/forms/common-themes/src/main/resources/theme/login/base/login.ftl b/forms/common-themes/src/main/resources/theme/login/base/login.ftl index 037dc49df8..4fd6f63aec 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/login.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login.ftl @@ -8,7 +8,7 @@
- +
@@ -33,7 +33,7 @@ ${rb.noAccount} ${rb.register} <#if realm.resetPasswordAllowed> - ${rb.loginForgot} ${rb.username} or ${rb.password}? + ${rb.loginForgot} ${rb.password}?
diff --git a/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties b/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties index 6dbef81721..79c8e7b19b 100644 --- a/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties +++ b/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties @@ -10,12 +10,13 @@ alreadyHaveAccount=Already have an account? poweredByKeycloak=Powered by Keycloak username=Username +usernameOrEmail=Username or email fullName=Full name firstName=First name lastName=Last name email=Email password=Password -passwordConfirm=Confirmation +passwordConfirm=Confirm password passwordNew=New Password passwordNewConfirm=New Password confirmation cancel=Cancel @@ -97,11 +98,7 @@ emailSent=You should receive an email shortly with further instructions. emailSendError=Failed to send email, please try again later emailError=Invalid email. emailErrorInfo=Please, fill in the fields again. -emailInstruction=Enter your email address and we will send you instructions on how to create a new password. - -emailUsernameForgotHeader=Forgot Your Username? -emailUsernameInstruction=Enter your email address and we will send you an email with your username. -emailUsernameSent=You should receive an email shortly with your username. +emailInstruction=Enter your username or email address and we will send you instructions on how to create a new password. accountUpdated=Your account has been updated accountPasswordUpdated=Your password has been updated \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/login/patternfly/resources/css/login.css b/forms/common-themes/src/main/resources/theme/login/patternfly/resources/css/login.css index e7ece07c04..5de3439431 100644 --- a/forms/common-themes/src/main/resources/theme/login/patternfly/resources/css/login.css +++ b/forms/common-themes/src/main/resources/theme/login/patternfly/resources/css/login.css @@ -24,6 +24,7 @@ #kc-login { float: right; margin-left: 10px; + margin-bottom: 10px; } diff --git a/forms/common-themes/src/main/resources/theme/login/patternfly/theme.properties b/forms/common-themes/src/main/resources/theme/login/patternfly/theme.properties index 64be5599bd..4b46ae1229 100644 --- a/forms/common-themes/src/main/resources/theme/login/patternfly/theme.properties +++ b/forms/common-themes/src/main/resources/theme/login/patternfly/theme.properties @@ -15,10 +15,10 @@ kcFormAreaClass=col-sm-7 col-md-6 col-lg-5 login kcFormClass=form-horizontal kcFormGroupClass=form-group kcLabelClass=control-label -kcLabelWrapperClass=col-sm-2 col-md-2 +kcLabelWrapperClass=col-sm-4 col-md-4 col-lg-3 kcInputClass=form-control -kcInputWrapperClass=col-sm-10 col-md-10 -kcFormOptionsClass=col-xs-8 col-sm-offset-2 col-sm-5 col-md-offset-2 col-md-5 -kcFormButtonsClass=col-xs-4 col-sm-5 col-md-5 submit +kcInputWrapperClass=col-sm-8 col-md-8 col-lg-9 +kcFormOptionsClass=col-sm-offset-4 col-sm-4 col-md-offset-4 col-md-4 col-lg-offset-3 col-lg-5 +kcFormButtonsClass=col-sm-4 col-md-4 col-lg-4 submit kcInfoAreaClass=col-sm-5 col-md-6 col-lg-7 details \ No newline at end of file diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginForms.java b/forms/login-api/src/main/java/org/keycloak/login/LoginForms.java index c693be289c..3cce80c972 100644 --- a/forms/login-api/src/main/java/org/keycloak/login/LoginForms.java +++ b/forms/login-api/src/main/java/org/keycloak/login/LoginForms.java @@ -18,8 +18,6 @@ public interface LoginForms { public Response createPasswordReset(); - public Response createUsernameReminder(); - public Response createLoginTotp(); public Response createRegistration(); diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/Templates.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/Templates.java index ad96e3864b..a57e8a316f 100644 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/Templates.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/Templates.java @@ -23,8 +23,6 @@ public class Templates { return "login-reset-password.ftl"; case LOGIN_UPDATE_PASSWORD: return "login-update-password.ftl"; - case LOGIN_USERNAME_REMINDER: - return "login-username-reminder.ftl"; case REGISTER: return "register.ftl"; case ERROR: 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 1226590954..aec3d57b84 100755 --- a/services/src/main/java/org/keycloak/services/email/EmailSender.java +++ b/services/src/main/java/org/keycloak/services/email/EmailSender.java @@ -145,16 +145,6 @@ public class EmailSender { send(user.getEmail(), "Reset password link", sb.toString()); } - public void sendUsernameReminder(UserModel user) throws EmailException { - StringBuilder sb = getHeader(user); - - sb.append("The username for your Keycloak account is ").append(user.getLoginName()).append(".\n"); - - addFooter(sb); - - send(user.getEmail(), "Username reminder", sb.toString()); - } - private StringBuilder getHeader(UserModel user) { StringBuilder sb = new StringBuilder(); 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 b38df73705..2d6268ce80 100755 --- a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java @@ -237,7 +237,7 @@ public class RequiredActionsService { @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response sendPasswordReset(final MultivaluedMap formData) { - String email = formData.getFirst("email"); + String username = formData.getFirst("username"); String scopeParam = uriInfo.getQueryParameters().getFirst("scope"); String state = uriInfo.getQueryParameters().getFirst("state"); @@ -254,67 +254,32 @@ public class RequiredActionsService { "Login requester not enabled."); } - UserModel user = realm.getUserByEmail(email); - if (user == null) { - return Flows.forms(realm, request, uriInfo).setError("emailError").createPasswordReset(); + UserModel user = realm.getUser(username); + if (user == null && username.contains("@")) { + user = realm.getUserByEmail(username); } - Set requiredActions = new HashSet(user.getRequiredActions()); - requiredActions.add(RequiredAction.UPDATE_PASSWORD); + if (user == null) { + logger.warn("Failed to send password reset email: user not found"); + } else { + Set requiredActions = new HashSet(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()); + AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user); + accessCode.setRequiredActions(requiredActions); + accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction()); - try { - new EmailSender(realm.getSmtpConfig()).sendPasswordReset(user, realm, accessCode, uriInfo); - } catch (EmailException e) { - logger.error("Failed to send password reset email", e); - return Flows.forms(realm, request, uriInfo).setError("emailSendError").createErrorPage(); + try { + new EmailSender(realm.getSmtpConfig()).sendPasswordReset(user, realm, accessCode, uriInfo); + } catch (EmailException e) { + logger.error("Failed to send password reset email", e); + return Flows.forms(realm, request, uriInfo).setError("emailSendError").createErrorPage(); + } } return Flows.forms(realm, request, uriInfo).setSuccess("emailSent").createPasswordReset(); } - - @Path("username-reminder") - @GET - public Response usernameReminder() { - return Flows.forms(realm, request, uriInfo).createUsernameReminder(); - } - - @Path("username-reminder") - @POST - @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - public Response sendUsernameReminder(final MultivaluedMap formData) { - String email = formData.getFirst("email"); - 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.getUserByEmail(email); - if (user == null) { - return Flows.forms(realm, request, uriInfo).setError("emailError").createUsernameReminder(); - } - - try { - new EmailSender(realm.getSmtpConfig()).sendUsernameReminder(user); - } catch (EmailException e) { - logger.error("Failed to send username reminder email", e); - return Flows.forms(realm, request, uriInfo).setError("emailSendError").createErrorPage(); - } - - return Flows.forms(realm, request, uriInfo).setSuccess("emailUsernameSent").createLogin(); - } - private AccessCodeEntry getAccessCodeEntry(RequiredAction requiredAction) { String code = uriInfo.getQueryParameters().getFirst("code"); if (code == null) { 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 19995eba5c..f38c985174 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -197,6 +197,9 @@ public class TokenService { String username = formData.getFirst("username"); UserModel user = realm.getUser(username); + if (user == null && username.contains("@")) { + user = realm.getUserByEmail(username); + } if (user == null){ return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginRecoverUsernameTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginRecoverUsernameTest.java deleted file mode 100755 index 9b37861dcc..0000000000 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginRecoverUsernameTest.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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.forms; - -import org.junit.Assert; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.Test; -import org.keycloak.testsuite.OAuthClient; -import org.keycloak.testsuite.pages.AppPage; -import org.keycloak.testsuite.pages.AppPage.RequestType; -import org.keycloak.testsuite.pages.LoginPage; -import org.keycloak.testsuite.pages.LoginPasswordResetPage; -import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; -import org.keycloak.testsuite.pages.LoginRecoverUsernamePage; -import org.keycloak.testsuite.rule.GreenMailRule; -import org.keycloak.testsuite.rule.KeycloakRule; -import org.keycloak.testsuite.rule.WebResource; -import org.keycloak.testsuite.rule.WebRule; -import org.openqa.selenium.WebDriver; - -import javax.mail.MessagingException; -import javax.mail.internet.MimeMessage; -import java.io.IOException; - -/** - * @author Stian Thorgersen - */ -public class LoginRecoverUsernameTest { - - @ClassRule - public static KeycloakRule keycloakRule = new KeycloakRule(); - - @Rule - public WebRule webRule = new WebRule(this); - - @Rule - public GreenMailRule greenMail = new GreenMailRule(); - - @WebResource - protected WebDriver driver; - - @WebResource - protected OAuthClient oauth; - - @WebResource - protected AppPage appPage; - - @WebResource - protected LoginPage loginPage; - - @WebResource - protected LoginRecoverUsernamePage recoverUsernamePage; - - @Test - public void resetPassword() throws IOException, MessagingException { - loginPage.open(); - loginPage.recoverUsername(); - - recoverUsernamePage.assertCurrent(); - - recoverUsernamePage.recoverUsername("test-user@localhost"); - - loginPage.assertCurrent(); - - Assert.assertTrue(driver.getPageSource().contains("You should receive an email shortly with your username")); - - Assert.assertEquals(1, greenMail.getReceivedMessages().length); - - MimeMessage message = greenMail.getReceivedMessages()[0]; - - String body = (String) message.getContent(); - Assert.assertTrue(body.contains("The username for your Keycloak account is test-user@localhost")); - } - - @Test - public void resetPasswordWrongEmail() throws IOException, MessagingException { - loginPage.open(); - loginPage.recoverUsername(); - - recoverUsernamePage.assertCurrent(); - - recoverUsernamePage.recoverUsername("invalid"); - - recoverUsernamePage.assertCurrent(); - - Assert.assertEquals("Invalid email.", recoverUsernamePage.getMessage()); - } - -} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java index 23750c1cfb..61a312e3ec 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java @@ -25,6 +25,11 @@ import org.junit.Assert; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; @@ -40,7 +45,20 @@ import org.openqa.selenium.WebDriver; public class LoginTest { @ClassRule - public static KeycloakRule keycloakRule = new KeycloakRule(); + public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + UserModel user = appRealm.addUser("login-test"); + user.setEmail("login@test.com"); + user.setEnabled(true); + + UserCredentialModel creds = new UserCredentialModel(); + creds.setType(CredentialRepresentation.PASSWORD); + creds.setValue("password"); + + appRealm.updateCredential(user, creds); + } + }); @Rule public WebRule webRule = new WebRule(this); @@ -48,7 +66,6 @@ public class LoginTest { @WebResource protected OAuthClient oauth; - @WebResource protected WebDriver driver; @@ -61,7 +78,7 @@ public class LoginTest { @Test public void loginInvalidPassword() { loginPage.open(); - loginPage.login("test-user@localhost", "invalid"); + loginPage.login("login-test", "invalid"); loginPage.assertCurrent(); @@ -81,12 +98,21 @@ public class LoginTest { @Test public void loginSuccess() { loginPage.open(); - loginPage.login("test-user@localhost", "password"); + loginPage.login("login-test", "password"); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get("code")); } + @Test + public void loginWithEmailSuccess() { + loginPage.open(); + loginPage.login("login@test.com", "password"); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + Assert.assertNotNull(oauth.getCurrentQuery().get("code")); + } + @Test public void loginCancel() { loginPage.open(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java index 2febe44b04..96f54283af 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java @@ -27,6 +27,9 @@ import org.junit.Rule; import org.junit.Test; import org.keycloak.models.PasswordPolicy; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.pages.AppPage; @@ -50,7 +53,20 @@ import java.io.IOException; public class ResetPasswordTest { @ClassRule - public static KeycloakRule keycloakRule = new KeycloakRule(); + public static KeycloakRule keycloakRule = new KeycloakRule((new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + UserModel user = appRealm.addUser("login-test"); + user.setEmail("login@test.com"); + user.setEnabled(true); + + UserCredentialModel creds = new UserCredentialModel(); + creds.setType(CredentialRepresentation.PASSWORD); + creds.setValue("password"); + + appRealm.updateCredential(user, creds); + } + })); @Rule public WebRule webRule = new WebRule(this); @@ -83,7 +99,7 @@ public class ResetPasswordTest { resetPasswordPage.assertCurrent(); - resetPasswordPage.changePassword("test-user@localhost"); + resetPasswordPage.changePassword("login-test"); resetPasswordPage.assertCurrent(); @@ -100,7 +116,7 @@ public class ResetPasswordTest { updatePasswordPage.assertCurrent(); - updatePasswordPage.changePassword("new-password", "new-password"); + updatePasswordPage.changePassword("resetPassword", "resetPassword"); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); @@ -108,13 +124,50 @@ public class ResetPasswordTest { loginPage.open(); - loginPage.login("test-user@localhost", "new-password"); + loginPage.login("login-test", "resetPassword"); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); } @Test - public void resetPasswordWrongEmail() throws IOException, MessagingException { + public void resetPasswordByEmail() throws IOException, MessagingException { + loginPage.open(); + loginPage.resetPassword(); + + resetPasswordPage.assertCurrent(); + + resetPasswordPage.changePassword("login@test.com"); + + resetPasswordPage.assertCurrent(); + + Assert.assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); + + Assert.assertEquals(1, greenMail.getReceivedMessages().length); + + MimeMessage message = greenMail.getReceivedMessages()[0]; + + String body = (String) message.getContent(); + String changePasswordUrl = body.split("\n")[3]; + + driver.navigate().to(changePasswordUrl.trim()); + + updatePasswordPage.assertCurrent(); + + updatePasswordPage.changePassword("resetPassword", "resetPassword"); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + oauth.openLogout(); + + loginPage.open(); + + loginPage.login("login@test.com", "resetPassword"); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + } + + @Test + public void resetPasswordWrongEmail() throws IOException, MessagingException, InterruptedException { loginPage.open(); loginPage.resetPassword(); @@ -124,7 +177,11 @@ public class ResetPasswordTest { resetPasswordPage.assertCurrent(); - Assert.assertEquals("Invalid email.", resetPasswordPage.getErrorMessage()); + Assert.assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); + + Thread.sleep(1000); + + Assert.assertEquals(0, greenMail.getReceivedMessages().length); } @Test @@ -141,7 +198,7 @@ public class ResetPasswordTest { resetPasswordPage.assertCurrent(); - resetPasswordPage.changePassword("test-user@localhost"); + resetPasswordPage.changePassword("login-test"); resetPasswordPage.assertCurrent(); @@ -162,7 +219,7 @@ public class ResetPasswordTest { Assert.assertEquals("Invalid password: minimum length 8", resetPasswordPage.getErrorMessage()); - updatePasswordPage.changePassword("new-password", "new-password"); + updatePasswordPage.changePassword("resetPasswordWithPasswordPolicy", "resetPasswordWithPasswordPolicy"); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); @@ -170,7 +227,7 @@ public class ResetPasswordTest { loginPage.open(); - loginPage.login("test-user@localhost", "new-password"); + loginPage.login("login-test", "resetPasswordWithPasswordPolicy"); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java index 758bd9fd39..8817c9c6e3 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java @@ -29,8 +29,8 @@ import org.openqa.selenium.support.FindBy; */ public class LoginPasswordResetPage extends AbstractPage { - @FindBy(id = "email") - private WebElement emailInput; + @FindBy(id = "username") + private WebElement usernameInput; @FindBy(css = "input[type=\"submit\"]") private WebElement submitButton; @@ -41,8 +41,8 @@ public class LoginPasswordResetPage extends AbstractPage { @FindBy(className = "feedback-error") private WebElement emailErrorMessage; - public void changePassword(String email) { - emailInput.sendKeys(email); + public void changePassword(String username) { + usernameInput.sendKeys(username); submitButton.click(); }