keycloak-aplcache

Changes

Details

diff --git a/forms/src/main/java/org/keycloak/forms/MessageBean.java b/forms/src/main/java/org/keycloak/forms/MessageBean.java
index c24e66a..85eee93 100644
--- a/forms/src/main/java/org/keycloak/forms/MessageBean.java
+++ b/forms/src/main/java/org/keycloak/forms/MessageBean.java
@@ -47,6 +47,10 @@ public class MessageBean {
         return summary;
     }
 
+    public String getType() {
+        return this.type.toString().toLowerCase();
+    }
+
     public boolean isSuccess(){
         return FormFlows.MessageType.SUCCESS.equals(this.type);
     }
diff --git a/forms/src/main/java/org/keycloak/forms/RealmBean.java b/forms/src/main/java/org/keycloak/forms/RealmBean.java
index 7b5e6b2..6eeabab 100755
--- a/forms/src/main/java/org/keycloak/forms/RealmBean.java
+++ b/forms/src/main/java/org/keycloak/forms/RealmBean.java
@@ -62,5 +62,9 @@ public class RealmBean {
     public boolean isRegistrationAllowed() {
         return realm.isRegistrationAllowed();
     }
+
+    public boolean isResetPasswordAllowed() {
+        return realm.isResetPasswordAllowed();
+    }
     
 }
diff --git a/forms/src/main/java/org/keycloak/forms/UrlBean.java b/forms/src/main/java/org/keycloak/forms/UrlBean.java
index 579659d..f6f5f12 100755
--- a/forms/src/main/java/org/keycloak/forms/UrlBean.java
+++ b/forms/src/main/java/org/keycloak/forms/UrlBean.java
@@ -135,6 +135,10 @@ public class UrlBean {
         return Urls.loginPasswordReset(baseURI, realm.getId()).toString();
     }
 
+    public String getLoginUsernameReminderUrl() {
+        return Urls.loginUsernameReminder(baseURI, realm.getId()).toString();
+    }
+
     public String getLoginEmailVerificationUrl() {
         return Urls.loginActionEmailVerification(baseURI, realm.getId()).toString();
     }
diff --git a/forms/src/main/java/org/keycloak/service/FormServiceImpl.java b/forms/src/main/java/org/keycloak/service/FormServiceImpl.java
index 596d893..01ca837 100755
--- a/forms/src/main/java/org/keycloak/service/FormServiceImpl.java
+++ b/forms/src/main/java/org/keycloak/service/FormServiceImpl.java
@@ -64,6 +64,7 @@ public class FormServiceImpl implements FormService {
         commandMap.put(Pages.LOGIN_UPDATE_PROFILE, new CommandCommon());
         commandMap.put(Pages.PASSWORD, new CommandCommon());
         commandMap.put(Pages.LOGIN_RESET_PASSWORD, new CommandCommon());
+        commandMap.put(Pages.LOGIN_USERNAME_REMINDER, new CommandCommon());
         commandMap.put(Pages.LOGIN_UPDATE_PASSWORD, new CommandCommon());
         commandMap.put(Pages.ACCESS, new CommandCommon());
         commandMap.put(Pages.SOCIAL, new CommandCommon());
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 248f17b..518a92c 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,10 +24,6 @@
                 <input class="btn-primary" name="login" type="submit" value="Log In"/>
                 <input class="btn-secondary" name="cancel" type="submit" value="Cancel"/>
             </div>
-
-            <div class="aside-btn">
-                <p>Forgot <a href="${url.loginPasswordResetUrl}">Password</a>?</p>
-            </div>
         </form>
     </div>
 
@@ -37,6 +33,9 @@
         <#if realm.registrationAllowed>
             <p>${rb.getString('noAccount')} <a href="${url.registrationUrl}">${rb.getString('register')}</a>.</p>
         </#if>
+        <#if realm.resetPasswordAllowed>
+            <p>Forgot <a href="${url.loginUsernameReminderUrl}">Username</a> / <a href="${url.loginPasswordResetUrl}">Password</a>?</p>
+        </#if>
     </div>
 
     </#if>
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-username-reminder.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-username-reminder.ftl
new file mode 100755
index 0000000..a00d87e
--- /dev/null
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-username-reminder.ftl
@@ -0,0 +1,25 @@
+<#import "template-login-action.ftl" as layout>
+<@layout.registrationLayout bodyClass="reset" isSeparator=true forceSeparator=true; section>
+    <#if section = "title">
+
+    ${rb.getString('emailUsernameForgotHeader')}
+
+    <#elseif section = "header">
+
+    ${rb.getString('emailUsernameForgotHeader')}
+
+    <#elseif section = "form">
+
+    <div id="form">
+        <p class="instruction">${rb.getString('emailUsernameInstruction')}</p>
+        <form action="${url.loginUsernameReminderUrl}" method="post">
+        	<div>
+      	    	<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" >
+        <p><a href="${url.loginUrl}">&laquo; Back to Login</a></p>
+    </#if>
+</@layout.registrationLayout>
\ No newline at end of file
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl
index c46227b..30f422c 100644
--- a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl
@@ -44,6 +44,13 @@
                             </p>
                         </div>
                     </#if>
+                    <#if message?has_content && message.success>
+                        <div class="feedback success bottom-left show">
+                            <p>
+                                <strong>${rb.getString('successHeader')}</strong><br/>${message.summary}
+                            </p>
+                        </div>
+                    </#if>
                     <#nested "form">
                 </div>
 
diff --git a/forms/src/main/resources/org/keycloak/forms/messages.properties b/forms/src/main/resources/org/keycloak/forms/messages.properties
index 5353df4..bb08d42 100644
--- a/forms/src/main/resources/org/keycloak/forms/messages.properties
+++ b/forms/src/main/resources/org/keycloak/forms/messages.properties
@@ -64,5 +64,9 @@ emailError=Invalid username or email.
 emailErrorInfo=Please, fill in the fields again.
 emailInstruction=Enter your username and 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.
+
 accountUpdated=Your account has been updated
 accountPasswordUpdated=Your password has been updated
\ No newline at end of file
diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java
index ce76dcb..7771097 100755
--- a/model/api/src/main/java/org/keycloak/models/RealmModel.java
+++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java
@@ -86,6 +86,8 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa
 
     UserModel getUser(String name);
 
+    UserModel getUserByEmail(String email);
+
     UserModel addUser(String username);
 
     boolean removeUser(String name);
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 201f286..b6227cd 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -440,6 +440,15 @@ public class RealmAdapter implements RealmModel {
     }
 
     @Override
+    public UserModel getUserByEmail(String email) {
+        TypedQuery<UserEntity> query = em.createNamedQuery("getRealmUserByEmail", UserEntity.class);
+        query.setParameter("email", email);
+        query.setParameter("realm", realm);
+        List<UserEntity> results = query.getResultList();
+        return results.isEmpty()? null : new UserAdapter(results.get(0));
+    }
+
+    @Override
     public UserModel addUser(String username) {
         UserEntity entity = new UserEntity();
         entity.setLoginName(username);
diff --git a/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java b/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java
index fb2dca6..0c8bfc4 100755
--- a/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java
+++ b/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java
@@ -509,6 +509,14 @@ public class RealmAdapter implements RealmModel {
         return new UserAdapter(user, getIdm());
     }
 
+    @Override
+    public UserModel getUserByEmail(String email) {
+        IdentityQuery<User> query = getIdm().createIdentityQuery(User.class);
+        query.setParameter(User.EMAIL, email);
+        List<User> users = query.getResultList();
+        return users.isEmpty() ? null : new UserAdapter(users.get(0), getIdm());
+    }
+
     protected User findPicketlinkUser(String name) {
         return SampleModel.getUser(getIdm(), name);
     }
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 b2f72ce..ee2d33c 100755
--- a/services/src/main/java/org/keycloak/services/email/EmailSender.java
+++ b/services/src/main/java/org/keycloak/services/email/EmailSender.java
@@ -108,16 +108,17 @@ public class EmailSender {
 
         URI uri = builder.build(realm.getId());
 
-        StringBuilder sb = new StringBuilder();
-        sb.append("Hi ").append(user.getFirstName()).append(",\n\n");
+
+        StringBuilder sb = getHeader(user);
+
         sb.append("Someone has created a Keycloak account with this email address. ");
         sb.append("If this was you, click the link below to verify your email address:\n");
         sb.append(uri.toString());
         sb.append("\n\nThis link will expire within ").append(TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()));
         sb.append(" minutes.\n\n");
-        sb.append("If you didn't create this account, just ignore this message.\n\n");
-        sb.append("Thanks,\n");
-        sb.append("The Keycloak Team");
+        sb.append("If you didn't create this account, just ignore this message.\n");
+
+        addFooter(sb);
 
         send(user.getEmail(), "Verify email", sb.toString());
     }
@@ -128,19 +129,44 @@ public class EmailSender {
 
         URI uri = builder.build(realm.getId());
 
-        StringBuilder sb = new StringBuilder();
+        StringBuilder sb = getHeader(user);
 
-        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\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");
+        sb.append("If you don't want to reset your password, just ignore this message and nothing will be changed.\n");
+
+        addFooter(sb);
 
         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();
+
+        sb.append("Hi");
+        if (user.getFirstName() != null) {
+            sb.append(" ").append(user.getFirstName());
+        }
+        sb.append(",\n\n");
+        return sb;
+    }
+
+    private void addFooter(StringBuilder sb) {
+        sb.append("\nThanks,\nThe Keycloak Team");
+    }
+
+
 }
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 fea4f98..2059409 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
@@ -176,6 +176,10 @@ public class FormFlows {
         return forwardToForm(Pages.LOGIN_RESET_PASSWORD);
     }
 
+    public Response forwardToUsernameReminder() {
+        return forwardToForm(Pages.LOGIN_USERNAME_REMINDER);
+    }
+
     public Response forwardToLoginTotp() {
         return forwardToForm(Pages.LOGIN_TOTP);
     }
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Pages.java b/services/src/main/java/org/keycloak/services/resources/flows/Pages.java
index 9ee5ad4..72d10e5 100644
--- a/services/src/main/java/org/keycloak/services/resources/flows/Pages.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/Pages.java
@@ -46,6 +46,8 @@ public class Pages {
 
     public final static String LOGIN_UPDATE_PASSWORD = "login-update-password.ftl";
 
+    public final static String LOGIN_USERNAME_REMINDER = "login-username-reminder.ftl";
+
     public final static String REGISTER = "register.ftl";
 
     public final static String ERROR = "error.ftl";
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 169da3f..15d6d36 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
@@ -100,6 +100,14 @@ public class Urls {
         return requiredActionsBase(baseUri).path(RequiredActionsService.class, "passwordReset");
     }
 
+    public static URI loginUsernameReminder(URI baseUri, String realmId) {
+        return loginUsernameReminderBuilder(baseUri).build(realmId);
+    }
+
+    public static UriBuilder loginUsernameReminderBuilder(URI baseUri) {
+        return requiredActionsBase(baseUri).path(RequiredActionsService.class, "usernameReminder");
+    }
+
     private static UriBuilder realmBase(URI baseUri) {
         return UriBuilder.fromUri(baseUri).path(RealmsResource.class);
     }
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 8faeb01..e8417b3 100755
--- a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
@@ -276,6 +276,45 @@ public class RequiredActionsService {
         return Flows.forms(realm, request, uriInfo).setSuccess("emailSent").forwardToPasswordReset();
     }
 
+
+    @Path("username-reminder")
+    @GET
+    public Response usernameReminder() {
+        return Flows.forms(realm, request, uriInfo).forwardToUsernameReminder();
+    }
+
+    @Path("username-reminder")
+    @POST
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response sendUsernameReminder(final MultivaluedMap<String, String> 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").forwardToUsernameReminder();
+        }
+
+        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").forwardToErrorPage();
+        }
+
+        return Flows.forms(realm, request, uriInfo).setSuccess("emailUsernameSent").forwardToLogin();
+    }
+
     private AccessCodeEntry getAccessCodeEntry(RequiredAction requiredAction) {
         String code = uriInfo.getQueryParameters().getFirst(FormFlows.CODE);
         if (code == null) {
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
new file mode 100755
index 0000000..006bb26
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginRecoverUsernameTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 username or email.", recoverUsernamePage.getMessage());
+    }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java
index 779873f..3a1f72e 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java
@@ -56,6 +56,9 @@ public class LoginPage extends AbstractPage {
     @FindBy(linkText = "Password")
     private WebElement resetPasswordLink;
 
+    @FindBy(linkText = "Username")
+    private WebElement recoverUsernameLink;
+
     @FindBy(id = "loginError")
     private WebElement loginErrorMessage;
 
@@ -93,6 +96,11 @@ public class LoginPage extends AbstractPage {
         resetPasswordLink.click();
     }
 
+    public void recoverUsername() {
+        recoverUsernameLink.click();
+    }
+
+
     @Override
     public void open() {
         oauth.openLoginForm();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginRecoverUsernamePage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginRecoverUsernamePage.java
new file mode 100644
index 0000000..5c0050f
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginRecoverUsernamePage.java
@@ -0,0 +1,59 @@
+/*
+ * 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.pages;
+
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class LoginRecoverUsernamePage extends AbstractPage {
+
+    @FindBy(id = "email")
+    private WebElement emailInput;
+
+    @FindBy(css = "input[type=\"submit\"]")
+    private WebElement submitButton;
+
+    @FindBy(css = ".feedback > p > strong")
+    private WebElement emailErrorMessage;
+
+    public void recoverUsername(String email) {
+        emailInput.sendKeys(email);
+
+        submitButton.click();
+    }
+
+    public boolean isCurrent() {
+        return driver.getTitle().equals("Forgot Your Username?");
+    }
+
+    public void open() {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getMessage() {
+        return emailErrorMessage != null ? emailErrorMessage.getText() : null;
+    }
+
+}