From c7b5975ac19883fa5ffdbaec786b8bf44740ddae Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Sun, 16 Aug 2015 15:20:16 -0400 Subject: [PATCH] reset password refactor --- .../theme/base/login/validate-reset-email.ftl | 9 +- .../keycloak/models/ClientSessionModel.java | 3 +- .../AuthenticationFlowContext.java | 7 + .../AuthenticationFlowError.java | 3 +- .../AuthenticationProcessor.java | 49 ++++--- .../DefaultAuthenticationFlow.java | 3 + .../keycloak/authentication/FlowStatus.java | 8 +- .../resetcred/ResetCredentialChooseUser.java | 39 ++---- .../resetcred/ResetCredentialEmail.java | 51 +++++-- .../resources/LoginActionsService.java | 81 +---------- .../testsuite/account/AccountTest.java | 2 +- .../testsuite/forms/ResetPasswordTest.java | 131 ++++++++---------- .../ValidatePassworrdEmailResetPage.java | 73 ++++++++++ 13 files changed, 237 insertions(+), 222 deletions(-) create mode 100755 testsuite/integration/src/test/java/org/keycloak/testsuite/pages/ValidatePassworrdEmailResetPage.java diff --git a/forms/common-themes/src/main/resources/theme/base/login/validate-reset-email.ftl b/forms/common-themes/src/main/resources/theme/base/login/validate-reset-email.ftl index fd5fa65c6d..5217b042ef 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/validate-reset-email.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/validate-reset-email.ftl @@ -16,14 +16,9 @@
- -
- + +
diff --git a/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java b/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java index 105b6babe7..52705a2402 100755 --- a/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java +++ b/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java @@ -80,7 +80,8 @@ public interface ClientSessionModel { RECOVER_PASSWORD, AUTHENTICATE, SOCIAL_CALLBACK, - LOGGED_OUT + LOGGED_OUT, + RESET_CREDENTIALS } public enum ExecutionStatus { diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java index 26ec7bd862..f4b4431456 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java @@ -224,4 +224,11 @@ public interface AuthenticationFlowContext { * */ void cancelLogin(); + + /** + * Abort the current flow and restart it using the realm's browser login + * + * @return + */ + void resetBrowserLogin(); } diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java index ec60973356..e348b67b83 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java @@ -16,5 +16,6 @@ public enum AuthenticationFlowError { USER_CONFLICT, USER_TEMPORARILY_DISABLED, INTERNAL_ERROR, - UNKNOWN_USER + UNKNOWN_USER, + RESET_TO_BROWSER_LOGIN } diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index 9f3eab9437..5caff0b481 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -391,6 +391,11 @@ public class AuthenticationProcessor { Response response = protocol.cancelLogin(getClientSession()); forceChallenge(response); } + + @Override + public void resetBrowserLogin() { + this.status = FlowStatus.RESET_BROWSER_LOGIN; + } } public void logFailure() { @@ -434,6 +439,21 @@ public class AuthenticationProcessor { event.error(Errors.EXPIRED_CODE); return ErrorPage.error(session, Messages.EXPIRED_CODE); + } else if (e.getError() == AuthenticationFlowError.RESET_TO_BROWSER_LOGIN) { + resetFlow(getClientSession()); + AuthenticationProcessor processor = new AuthenticationProcessor(); + processor.setClientSession(clientSession) + .setFlowPath(LoginActionsService.AUTHENTICATE_PATH) + .setFlowId(realm.getBrowserFlow().getId()) + .setConnection(connection) + .setEventBuilder(event) + .setProtector(protector) + .setRealm(realm) + .setSession(session) + .setUriInfo(uriInfo) + .setRequest(request); + return processor.authenticate(); + } else { event.error(Errors.INVALID_USER_CREDENTIALS); return ErrorPage.error(session, Messages.INVALID_USER); @@ -530,10 +550,11 @@ public class AuthenticationProcessor { public void checkClientSession() { ClientSessionCode code = new ClientSessionCode(realm, clientSession); - if (!code.isValidAction(ClientSessionModel.Action.AUTHENTICATE.name())) { + String action = ClientSessionModel.Action.AUTHENTICATE.name(); + if (!code.isValidAction(action)) { throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CLIENT_SESSION); } - if (!code.isActionActive(ClientSessionModel.Action.AUTHENTICATE.name())) { + if (!code.isActionActive(action)) { throw new AuthenticationFlowException(AuthenticationFlowError.EXPIRED_CODE); } clientSession.setTimestamp(Time.currentTime()); @@ -564,12 +585,16 @@ public class AuthenticationProcessor { String username = clientSession.getAuthenticatedUser().getUsername(); String attemptedUsername = clientSession.getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME); if (attemptedUsername != null) username = attemptedUsername; + String rememberMe = clientSession.getNote(Details.REMEMBER_ME); + boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("true"); if (userSession == null) { // if no authenticator attached a usersession - boolean remember = "true".equals(clientSession.getNote(Details.REMEMBER_ME)); - userSession = session.sessions().createUserSession(realm, clientSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), "form", remember, null, null); + userSession = session.sessions().createUserSession(realm, clientSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), clientSession.getAuthMethod(), remember, null, null); userSession.setState(UserSessionModel.State.LOGGING_IN); userSessionCreated = true; } + if (remember) { + event.detail(Details.REMEMBER_ME, "true"); + } TokenManager.attachClientSession(userSession, clientSession); event.user(userSession.getUser()) .detail(Details.USERNAME, username) @@ -598,21 +623,7 @@ public class AuthenticationProcessor { } protected Response authenticationComplete() { - String username = clientSession.getAuthenticatedUser().getUsername(); - String rememberMe = clientSession.getNote(Details.REMEMBER_ME); - boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("true"); - if (userSession == null) { // if no authenticator attached a usersession - userSession = session.sessions().createUserSession(realm, clientSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), clientSession.getAuthMethod(), remember, null, null); - userSession.setState(UserSessionModel.State.LOGGING_IN); - } - if (remember) { - event.detail(Details.REMEMBER_ME, "true"); - } - TokenManager.attachClientSession(userSession, clientSession); - event.user(userSession.getUser()) - .detail(Details.USERNAME, username) - .session(userSession); - + attachSession(); return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, connection, request, uriInfo, event); } diff --git a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java index 49c48ad543..df9a26bb02 100755 --- a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java +++ b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java @@ -166,6 +166,9 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { return sendChallenge(result, execution); } throw new AuthenticationFlowException(result.getError()); + } else if (status == FlowStatus.RESET_BROWSER_LOGIN) { + AuthenticationProcessor.logger.debugv("reset browser login from authenticator: {0}", execution.getAuthenticator()); + throw new AuthenticationFlowException(AuthenticationFlowError.RESET_TO_BROWSER_LOGIN); } else if (status == FlowStatus.FORCE_CHALLENGE) { processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); return sendChallenge(result, execution); diff --git a/services/src/main/java/org/keycloak/authentication/FlowStatus.java b/services/src/main/java/org/keycloak/authentication/FlowStatus.java index 0acd8755a2..31b7eee7c7 100755 --- a/services/src/main/java/org/keycloak/authentication/FlowStatus.java +++ b/services/src/main/java/org/keycloak/authentication/FlowStatus.java @@ -42,6 +42,12 @@ public enum FlowStatus { * a Kerberos authenticator did not see a negotiate header. There was no error, but the execution was attempted. * */ - ATTEMPTED + ATTEMPTED, + + /** + * Aborting this flow and starting the realm's browser flow from the beginning + * + */ + RESET_BROWSER_LOGIN } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java index 1edd53586f..a62449536c 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java @@ -5,6 +5,7 @@ import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator; import org.keycloak.email.EmailException; import org.keycloak.email.EmailProvider; import org.keycloak.events.Details; @@ -62,34 +63,22 @@ public class ResetCredentialChooseUser implements Authenticator, AuthenticatorFa user = context.getSession().users().getUserByEmail(username, context.getRealm()); } + context.getClientSession().setNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username); + + // we don't want people guessing usernames, so if there is a problem, just continue, but don't set the user + // a null user will notify further executions, that this was a failure. if (user == null) { - event.error(Errors.INVALID_USER_CREDENTIALS); - Response challenge = context.form() - .setError(Messages.INVALID_USER) - .createPasswordReset(); - context.failureChallenge(AuthenticationFlowError.INVALID_USER, challenge); - return; + event.clone() + .detail(Details.USERNAME, username) + .error(Errors.USER_NOT_FOUND); + } else if (!user.isEnabled()) { + event.clone() + .detail(Details.USERNAME, username) + .user(user).error(Errors.USER_DISABLED); + } else { + context.setUser(user); } - if (!user.isEnabled()) { - event.user(user).error(Errors.USER_DISABLED); - Response challenge = context.form() - .setError(Messages.ACCOUNT_DISABLED) - .createPasswordReset(); - context.failureChallenge(AuthenticationFlowError.INVALID_USER, challenge); - return; - } - - if (user.getEmail() == null || user.getEmail().trim().length() == 0) { - event.user(user).error(Errors.INVALID_EMAIL); - Response challenge = context.form() - .setError(Messages.INVALID_EMAIL) - .createPasswordReset(); - context.failureChallenge(AuthenticationFlowError.INVALID_USER, challenge); - return; - } - - context.setUser(user); context.success(); } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java index 5fb3c00145..f5e0165928 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java @@ -7,11 +7,13 @@ import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator; import org.keycloak.email.EmailException; import org.keycloak.email.EmailProvider; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.jose.jws.JWSInput; import org.keycloak.login.LoginFormsProvider; @@ -49,13 +51,29 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory @Override public void authenticate(AuthenticationFlowContext context) { UserModel user = context.getUser(); - EventBuilder event = context.getEvent(); - if (user.getEmail() == null || user.getEmail().trim().length() == 0) { - event.user(user).error(Errors.INVALID_EMAIL); + String username = context.getClientSession().getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME); + + // we don't want people guessing usernames, so if there was a problem obtaining the user, the user will be null. + // just redisplay this form + if (user == null) { Response challenge = context.form() - .setError(Messages.INVALID_EMAIL) - .createPasswordReset(); - context.failureChallenge(AuthenticationFlowError.INVALID_USER, challenge); + .setSuccess(Messages.EMAIL_SENT) + .createForm("validate-reset-email.ftl"); + context.challenge(challenge); + return; + } + + + EventBuilder event = context.getEvent(); + // we don't want people guessing usernames, so if there is a problem, just continuously challenge + if (user.getEmail() == null || user.getEmail().trim().length() == 0) { + event.user(user) + .detail(Details.USERNAME, username) + .error(Errors.INVALID_EMAIL); + Response challenge = context.form() + .setSuccess(Messages.EMAIL_SENT) + .createForm("validate-reset-email.ftl"); + context.challenge(challenge); return; } @@ -68,14 +86,19 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory try { context.getSession().getProvider(EmailProvider.class).setRealm(context.getRealm()).setUser(user).sendPasswordReset(secret, link, expiration); - - event.detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, context.getClientSession().getId()).success(); + event.clone().event(EventType.SEND_RESET_PASSWORD) + .user(user) + .detail(Details.USERNAME, username) + .detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, context.getClientSession().getId()).success(); Response challenge = context.form() .setSuccess(Messages.EMAIL_SENT) .createForm("validate-reset-email.ftl"); context.challenge(challenge); } catch (EmailException e) { - event.error(Errors.EMAIL_SEND_FAILED); + event.clone().event(EventType.SEND_RESET_PASSWORD) + .detail(Details.USERNAME, username) + .user(user) + .error(Errors.EMAIL_SEND_FAILED); logger.error("Failed to send password reset email", e); Response challenge = context.form() .setError(Messages.EMAIL_SENT_ERROR) @@ -92,7 +115,13 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory key =context.getUriInfo().getQueryParameters().getFirst(KEY); } else if (context.getHttpRequest().getHttpMethod().equalsIgnoreCase("POST")) { - key = context.getHttpRequest().getDecodedFormParameters().getFirst(KEY); + MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); + if (formData.containsKey("cancel")) { + context.resetBrowserLogin(); + return; + } + + key = formData.getFirst(KEY); } // Can only guess once! We remove the note so another guess can't happen @@ -110,7 +139,7 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory @Override public boolean requiresUser() { - return true; + return false; } @Override diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index f34bed652e..d87ad6880f 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -29,6 +29,7 @@ import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionContextResult; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator; import org.keycloak.email.EmailException; import org.keycloak.email.EmailProvider; import org.keycloak.events.Details; @@ -762,85 +763,6 @@ public class LoginActionsService { } } - private Response sendPasswordReset(@QueryParam("code") String code, - final MultivaluedMap formData) { - event.event(EventType.SEND_RESET_PASSWORD); - if (!realm.isResetPasswordAllowed()) { - event.error(Errors.RESET_CREDENTIAL_DISABLED); - return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED); - } - Checks checks = new Checks(); - if (!checks.verifyCode(code)) { - return checks.response; - } - final ClientSessionCode accessCode = checks.clientCode; - final ClientSessionModel clientSession = accessCode.getClientSession(); - ClientModel client = clientSession.getClient(); - - - String username = formData.getFirst("username"); - if (username == null || username.isEmpty()) { - event.error(Errors.USERNAME_MISSING); - return session.getProvider(LoginFormsProvider.class) - .setError(Messages.MISSING_USERNAME) - .setClientSessionCode(accessCode.getCode()) - .createPasswordReset(); - } - - event.client(client.getClientId()) - .detail(Details.REDIRECT_URI, clientSession.getRedirectUri()) - .detail(Details.RESPONSE_TYPE, "code") - .detail(Details.AUTH_METHOD, "form") - .detail(Details.USERNAME, username); - - UserModel user = session.users().getUserByUsername(username, realm); - if (user == null && username.contains("@")) { - user = session.users().getUserByEmail(username, realm); - } - - if (user == null) { - event.error(Errors.USER_NOT_FOUND); - } else if (!user.isEnabled()) { - event.user(user).error(Errors.USER_DISABLED); - } else if (user.getEmail() == null || user.getEmail().trim().length() == 0) { - event.user(user).error(Errors.INVALID_EMAIL); - } else { - event.user(user); - - UserSessionModel userSession = session.sessions().createUserSession(realm, user, username, clientConnection.getRemoteAddr(), "form", false, null, null); - event.session(userSession); - TokenManager.attachClientSession(userSession, clientSession); - - accessCode.setAction(ClientSessionModel.Action.RECOVER_PASSWORD.name()); - - try { - UriBuilder builder = Urls.loginResetCredentialsBuilder(uriInfo.getBaseUri()); - builder.queryParam("key", accessCode.getCode()); - - String link = builder.build(realm.getName()).toString(); - long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()); - - this.session.getProvider(EmailProvider.class).setRealm(realm).setUser(user).sendChangePassword(link, expiration); - - event.detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, clientSession.getId()).success(); - } catch (EmailException e) { - event.error(Errors.EMAIL_SEND_FAILED); - logger.error("Failed to send password reset email", e); - return session.getProvider(LoginFormsProvider.class) - .setError(Messages.EMAIL_SENT_ERROR) - .setClientSessionCode(accessCode.getCode()) - .createErrorPage(); - } - - createActionCookie(realm, uriInfo, clientConnection, userSession.getId()); - } - - return session.getProvider(LoginFormsProvider.class) - .setSuccess(Messages.EMAIL_SENT) - .setClientSessionCode(accessCode.getCode()) - .createPasswordReset(); - } - private String getActionCookie() { Cookie cookie = headers.getCookies().get(ACTION_COOKIE); AuthenticationManager.expireCookie(realm, ACTION_COOKIE, AuthenticationManager.getRealmCookiePath(realm, uriInfo), realm.getSslRequired().isRequired(clientConnection), clientConnection); @@ -857,6 +779,7 @@ public class LoginActionsService { .session(clientSession.getUserSession().getId()) .detail(Details.CODE_ID, clientSession.getId()) .detail(Details.REDIRECT_URI, clientSession.getRedirectUri()) + .detail(Details.USERNAME, clientSession.getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME)) .detail(Details.RESPONSE_TYPE, "code"); UserSessionModel userSession = clientSession.getUserSession(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java index bcaa98e257..95d644eb82 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java @@ -167,7 +167,7 @@ public class AccountTest { }); } - @Test + //@Test public void ideTesting() throws Exception { Thread.sleep(100000000); } 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 e3926ef1db..d8ae5fe965 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 @@ -44,6 +44,7 @@ import org.keycloak.testsuite.pages.InfoPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPasswordResetPage; import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; +import org.keycloak.testsuite.pages.ValidatePassworrdEmailResetPage; import org.keycloak.testsuite.rule.GreenMailRule; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.WebResource; @@ -65,6 +66,7 @@ import static org.junit.Assert.*; */ public class ResetPasswordTest { + static int lifespan = 0; @ClassRule public static KeycloakRule keycloakRule = new KeycloakRule((new KeycloakRule.KeycloakSetup() { @Override @@ -81,6 +83,7 @@ public class ResetPasswordTest { user.updateCredential(creds); appRealm.setEventsListeners(Collections.singleton("dummy")); + lifespan = appRealm.getAccessCodeLifespanUserAction(); } })); @@ -113,6 +116,9 @@ public class ResetPasswordTest { @WebResource protected LoginPasswordResetPage resetPasswordPage; + @WebResource + protected ValidatePassworrdEmailResetPage validateResetPage; + @WebResource protected LoginPasswordUpdatePage updatePasswordPage; @@ -133,12 +139,13 @@ public class ResetPasswordTest { resetPasswordPage.changePassword("login-test"); - resetPasswordPage.assertCurrent(); + validateResetPage.assertCurrent(); - events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId(); + events.expectRequiredAction(EventType.SEND_RESET_PASSWORD) + .session((String)null) + .user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId(); - String src = driver.getPageSource(); - resetPasswordPage.backToLogin(); + validateResetPage.cancel(); assertTrue(loginPage.isCurrent()); @@ -169,17 +176,19 @@ public class ResetPasswordTest { resetPasswordPage.changePassword("test-user@localhost"); - resetPasswordPage.assertCurrent(); + validateResetPage.assertCurrent(); - events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).detail(Details.USERNAME, "test-user@localhost").detail(Details.EMAIL, "test-user@localhost").assertEvent().getSessionId(); + events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).detail(Details.USERNAME, "test-user@localhost") + .session((String) null) + .detail(Details.EMAIL, "test-user@localhost").assertEvent(); - resetPasswordPage.backToLogin(); + validateResetPage.cancel(); assertTrue(loginPage.isCurrent()); loginPage.login("login@test.com", "password"); - Event loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent(); + Event loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "login@test.com").assertEvent(); String code = oauth.getCurrentQuery().get("code"); OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); @@ -203,9 +212,14 @@ public class ResetPasswordTest { resetPasswordPage.changePassword(username); - resetPasswordPage.assertCurrent(); + validateResetPage.assertCurrent(); - String sessionId = events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId(); + events.expectRequiredAction(EventType.SEND_RESET_PASSWORD) + .user(userId) + .detail(Details.USERNAME, username) + .detail(Details.EMAIL, "login@test.com") + .session((String)null) + .assertEvent(); assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); @@ -221,7 +235,7 @@ public class ResetPasswordTest { updatePasswordPage.changePassword("resetPassword", "resetPassword"); - events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).session(sessionId).detail(Details.USERNAME, username).assertEvent(); + String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, username).assertEvent().getSessionId(); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); @@ -248,10 +262,10 @@ public class ResetPasswordTest { resetPasswordPage.changePassword(username); - resetPasswordPage.assertCurrent(); + validateResetPage.assertCurrent(); - String sessionId = events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId) - .detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId(); + events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).session((String)null) + .detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent(); assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); @@ -265,12 +279,12 @@ public class ResetPasswordTest { updatePasswordPage.changePassword(password, password); - events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).session(sessionId) - .detail(Details.USERNAME, username).assertEvent(); + String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId) + .detail(Details.USERNAME, username).assertEvent().getSessionId(); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - events.expectLogin().user(userId).detail(Details.USERNAME, username).session(sessionId).assertEvent(); + events.expectLogin().user(userId).detail(Details.USERNAME, username).assertEvent(); oauth.openLogout(); @@ -285,10 +299,10 @@ public class ResetPasswordTest { resetPasswordPage.changePassword(username); - resetPasswordPage.assertCurrent(); + validateResetPage.assertCurrent(); - events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId) - .detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId(); + events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).session((String)null) + .detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent(); assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); @@ -315,13 +329,13 @@ public class ResetPasswordTest { resetPasswordPage.changePassword("invalid"); - resetPasswordPage.assertCurrent(); + validateResetPage.assertCurrent(); assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); assertEquals(0, greenMail.getReceivedMessages().length); - events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user((String) null).session((String) null).detail(Details.USERNAME, "invalid").removeDetail(Details.EMAIL).removeDetail(Details.CODE_ID).error("user_not_found").assertEvent(); + events.expectRequiredAction(EventType.RESET_PASSWORD).user((String) null).session((String) null).detail(Details.USERNAME, "invalid").removeDetail(Details.EMAIL).removeDetail(Details.CODE_ID).error("user_not_found").assertEvent(); } @Test @@ -339,7 +353,7 @@ public class ResetPasswordTest { assertEquals(0, greenMail.getReceivedMessages().length); - events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).client((String) null).user((String) null).session((String) null).clearDetails().error("username_missing").assertEvent(); + events.expectRequiredAction(EventType.RESET_PASSWORD).user((String) null).session((String) null).clearDetails().error("username_missing").assertEvent(); } @@ -353,9 +367,11 @@ public class ResetPasswordTest { resetPasswordPage.changePassword("login-test"); - resetPasswordPage.assertCurrent(); + validateResetPage.assertCurrent(); - String sessionId = events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId(); + events.expectRequiredAction(EventType.SEND_RESET_PASSWORD) + .session((String)null) + .user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent(); assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); @@ -365,13 +381,13 @@ public class ResetPasswordTest { String changePasswordUrl = getPasswordResetEmailLink(message); - Time.setOffset(350); + Time.setOffset(1800+23); driver.navigate().to(changePasswordUrl.trim()); - errorPage.assertCurrent(); + loginPage.assertCurrent(); - assertEquals("Login timeout. Please login again.", errorPage.getError()); + assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError()); events.expectRequiredAction(EventType.RESET_PASSWORD).error("expired_code").client("test-app").user((String) null).session((String) null).clearDetails().assertEvent(); } finally { @@ -396,13 +412,13 @@ public class ResetPasswordTest { resetPasswordPage.changePassword("login-test"); - resetPasswordPage.assertCurrent(); + validateResetPage.assertCurrent(); assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); assertEquals(0, greenMail.getReceivedMessages().length); - events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("user_disabled").assertEvent(); + events.expectRequiredAction(EventType.RESET_PASSWORD).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("user_disabled").assertEvent(); } finally { keycloakRule.configure(new KeycloakRule.KeycloakSetup() { @Override @@ -434,13 +450,13 @@ public class ResetPasswordTest { resetPasswordPage.changePassword("login-test"); - resetPasswordPage.assertCurrent(); + validateResetPage.assertCurrent(); assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); assertEquals(0, greenMail.getReceivedMessages().length); - events.expectRequiredAction(EventType.SEND_RESET_PASSWORD_ERROR).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("invalid_email").assertEvent(); + events.expectRequiredAction(EventType.RESET_PASSWORD_ERROR).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("invalid_email").assertEvent(); } finally { keycloakRule.configure(new KeycloakRule.KeycloakSetup() { @Override @@ -476,7 +492,9 @@ public class ResetPasswordTest { assertEquals(0, greenMail.getReceivedMessages().length); - events.expectRequiredAction(EventType.SEND_RESET_PASSWORD_ERROR).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error(Errors.EMAIL_SEND_FAILED).assertEvent(); + events.expectRequiredAction(EventType.SEND_RESET_PASSWORD_ERROR).user(userId) + .session((String)null) + .detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error(Errors.EMAIL_SEND_FAILED).assertEvent(); } finally { keycloakRule.configure(new KeycloakRule.KeycloakSetup() { @Override @@ -503,7 +521,7 @@ public class ResetPasswordTest { resetPasswordPage.changePassword("login-test"); - resetPasswordPage.assertCurrent(); + validateResetPage.assertCurrent(); assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); @@ -513,7 +531,7 @@ public class ResetPasswordTest { String changePasswordUrl = getPasswordResetEmailLink(message); - String sessionId = events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId(); + events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).session((String)null).user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent(); driver.navigate().to(changePasswordUrl.trim()); @@ -525,7 +543,7 @@ public class ResetPasswordTest { updatePasswordPage.changePassword("resetPasswordWithPasswordPolicy", "resetPasswordWithPasswordPolicy"); - events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).session(sessionId).detail(Details.USERNAME, "login-test").assertEvent(); + String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId(); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); @@ -585,47 +603,6 @@ public class ResetPasswordTest { } } - @Test - public void resetPasswordNewBrowserSession() throws IOException, MessagingException { - String username = "login-test"; - - loginPage.open(); - loginPage.resetPassword(); - - resetPasswordPage.assertCurrent(); - - resetPasswordPage.changePassword(username); - - resetPasswordPage.assertCurrent(); - - String sessionId = events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId(); - - assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); - - assertEquals(1, greenMail.getReceivedMessages().length); - - MimeMessage message = greenMail.getReceivedMessages()[0]; - - String changePasswordUrl = getPasswordResetEmailLink(message); - - driver.manage().deleteAllCookies(); - - driver.navigate().to(changePasswordUrl.trim()); - - updatePasswordPage.assertCurrent(); - - updatePasswordPage.changePassword("resetPassword", "resetPassword"); - - events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).session(sessionId).detail(Details.USERNAME, username).assertEvent(); - - assertTrue(infoPage.isCurrent()); - assertEquals("Your password has been updated.", infoPage.getInfo()); - - loginPage.open(); - - assertTrue(loginPage.isCurrent()); - } - private String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException { Multipart multipart = (Multipart) message.getContent(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/ValidatePassworrdEmailResetPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/ValidatePassworrdEmailResetPage.java new file mode 100755 index 0000000000..780b704c8a --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/ValidatePassworrdEmailResetPage.java @@ -0,0 +1,73 @@ +/* + * 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 Stian Thorgersen + */ +public class ValidatePassworrdEmailResetPage extends AbstractPage { + + @FindBy(id = "key") + private WebElement keyInput; + + @FindBy(id="kc-submit") + private WebElement submitButton; + + @FindBy(id="kc-cancel") + private WebElement cancelButton; + + @FindBy(className = "feedback-success") + private WebElement emailSuccessMessage; + + @FindBy(className = "feedback-error") + private WebElement emailErrorMessage; + + public void submitCode(String code) { + keyInput.sendKeys(code); + + submitButton.click(); + } + + public void cancel() { + cancelButton.click(); + } + + public boolean isCurrent() { + return driver.getTitle().equals("Forgot Your Password?"); + } + + public void open() { + throw new UnsupportedOperationException(); + } + + public String getSuccessMessage() { + return emailSuccessMessage != null ? emailSuccessMessage.getText() : null; + } + + public String getErrorMessage() { + return emailErrorMessage != null ? emailErrorMessage.getText() : null; + } + +}