diff --git a/forms/src/main/java/org/keycloak/forms/UrlBean.java b/forms/src/main/java/org/keycloak/forms/UrlBean.java index b6e2ff3016..86b9a6dd54 100644 --- a/forms/src/main/java/org/keycloak/forms/UrlBean.java +++ b/forms/src/main/java/org/keycloak/forms/UrlBean.java @@ -96,6 +96,10 @@ public class UrlBean { } } + public String getPasswordResetUrl() { + return Urls.accountPasswordReset(baseURI, realm.getId()).toString(); + } + public String getSocialUrl() { return Urls.accountSocialPage(baseURI, realm.getId()).toString(); } @@ -104,4 +108,8 @@ public class UrlBean { return Urls.accountTotpPage(baseURI, realm.getId()).toString(); } + public String getEmailVerificationUrl() { + return Urls.accountEmailVerification(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 41482ad556..a892454694 100644 --- a/forms/src/main/java/org/keycloak/service/FormServiceImpl.java +++ b/forms/src/main/java/org/keycloak/service/FormServiceImpl.java @@ -59,13 +59,17 @@ public class FormServiceImpl implements FormService { commandMap.put(Pages.LOGIN, new CommandLogin()); commandMap.put(Pages.REGISTER, new CommandRegister()); commandMap.put(Pages.ACCOUNT, new CommandAccount()); + commandMap.put(Pages.LOGIN_UPDATE_PROFILE, new CommandPassword()); commandMap.put(Pages.PASSWORD, new CommandPassword()); + commandMap.put(Pages.LOGIN_RESET_PASSWORD, new CommandPassword()); + commandMap.put(Pages.LOGIN_UPDATE_PASSWORD, new CommandPassword()); commandMap.put(Pages.ACCESS, new CommandAccess()); - commandMap.put(Pages.LOGIN_TOTP, new CommandLoginTotp()); commandMap.put(Pages.SECURITY_FAILURE, new CommandSecurityFailure()); commandMap.put(Pages.SOCIAL, new CommandSocial()); commandMap.put(Pages.TOTP, new CommandTotp()); - commandMap.put(Pages.VERIFY_EMAIL, new CommandEmail()); + commandMap.put(Pages.LOGIN_CONFIG_TOTP, new CommandTotp()); + commandMap.put(Pages.LOGIN_TOTP, new CommandLoginTotp()); + commandMap.put(Pages.LOGIN_VERIFY_EMAIL, new CommandLoginTotp()); } public String getId(){ diff --git a/forms/src/main/resources/META-INF/resources/forms/login-config-totp.ftl b/forms/src/main/resources/META-INF/resources/forms/login-config-totp.ftl new file mode 100644 index 0000000000..15fbecc1e9 --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/login-config-totp.ftl @@ -0,0 +1 @@ +<#include "./theme/" + template.theme + "/login-config-totp.ftl"> \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/login-reset-password.ftl b/forms/src/main/resources/META-INF/resources/forms/login-reset-password.ftl new file mode 100644 index 0000000000..69052d0624 --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/login-reset-password.ftl @@ -0,0 +1 @@ +<#include "./theme/" + template.theme + "/login-reset-password.ftl"> \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/login-update-password.ftl b/forms/src/main/resources/META-INF/resources/forms/login-update-password.ftl new file mode 100644 index 0000000000..fcf2e69e36 --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/login-update-password.ftl @@ -0,0 +1 @@ +<#include "./theme/" + template.theme + "/login-update-password.ftl"> \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/login-update-profile.ftl b/forms/src/main/resources/META-INF/resources/forms/login-update-profile.ftl new file mode 100644 index 0000000000..c05cd6c057 --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/login-update-profile.ftl @@ -0,0 +1 @@ +<#include "./theme/" + template.theme + "/login-update-profile.ftl"> \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/login-verify-email.ftl b/forms/src/main/resources/META-INF/resources/forms/login-verify-email.ftl new file mode 100644 index 0000000000..64af78478b --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/login-verify-email.ftl @@ -0,0 +1 @@ +<#include "./theme/" + template.theme + "/login-verify-email.ftl"> \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-config-totp.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-config-totp.ftl new file mode 100755 index 0000000000..bceee99dc0 --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-config-totp.ftl @@ -0,0 +1,42 @@ +<#import "template-login-action.ftl" as layout> +<@layout.registrationLayout bodyClass=""; section> + <#if section = "title"> + + Config TOTP + + <#elseif section = "header"> + + Config TOTP + + <#elseif section = "form"> + +
+

To setup Google Authenticator

+ +
    +
  1. Install Google Authenticator to your device
  2. +
  3. Set up an account in Google Authenticator and scan the QR code below or enter the key
    + ${totp.totpSecretEncoded} +
  4. +
  5. Enter a one-time password provided by Google Authenticator and click Save to finish the setup + +
    +
    + + + +
    + + +
    +
  6. +
+
+ + <#elseif section = "info" > + +
+
+ + + \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-reset-password.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-reset-password.ftl new file mode 100755 index 0000000000..2e3a71567a --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-reset-password.ftl @@ -0,0 +1,34 @@ +<#import "template-login-action.ftl" as layout> +<@layout.registrationLayout bodyClass=""; section> + <#if section = "title"> + + Reset password + + <#elseif section = "header"> + + Reset password + + <#elseif section = "form"> + +
+
+
+ + +
+
+ + +
+ + +
+
+ + <#elseif section = "info" > + +
+
+ + + \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.ftl index 5ca1109267..88e61a0369 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.ftl +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.ftl @@ -1,4 +1,4 @@ -<#import "template-login.ftl" as layout> +<#import "template-login-action.ftl" as layout> <@layout.registrationLayout bodyClass=""; section> <#if section = "title"> diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-password.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-password.ftl new file mode 100755 index 0000000000..fdfcb2cb11 --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-password.ftl @@ -0,0 +1,34 @@ +<#import "template-login-action.ftl" as layout> +<@layout.registrationLayout bodyClass=""; section> + <#if section = "title"> + + Update password + + <#elseif section = "header"> + + Update password + + <#elseif section = "form"> + +
+
+
+ + +
+
+ + +
+ + +
+
+ + <#elseif section = "info" > + +
+
+ + + \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-profile.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-profile.ftl new file mode 100755 index 0000000000..84bab40db4 --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-profile.ftl @@ -0,0 +1,41 @@ +<#import "template-login-action.ftl" as layout> +<@layout.registrationLayout bodyClass=""; section> + <#if section = "title"> + + Update profile + + <#elseif section = "header"> + + Update profile + + <#elseif section = "form"> + +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + <#elseif section = "info" > + +
+
+ + + \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-verify-email.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-verify-email.ftl new file mode 100755 index 0000000000..bd2b14114c --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-verify-email.ftl @@ -0,0 +1,24 @@ +<#import "template-login-action.ftl" as layout> +<@layout.registrationLayout bodyClass=""; section> + <#if section = "title"> + + Verify email + + <#elseif section = "header"> + + Verify email + + <#elseif section = "form"> + +
+ An email with instructions to verify your email address has been sent to you. If you don't receive this email, + click here to re-send the email. +
+ + <#elseif section = "info" > + +
+
+ + + \ No newline at end of file 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 80994d6646..e74b40facb 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 @@ -36,6 +36,8 @@ <#if realm.registrationAllowed>

${rb.getString('noAccount')} ${rb.getString('register')}.

+ + Reset password diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login-action.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login-action.ftl new file mode 100644 index 0000000000..2b6074b7da --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login-action.ftl @@ -0,0 +1,63 @@ +<#macro registrationLayout bodyClass> + + + + + + + <#nested "title"> + + + + + + + <#if (template.themeConfig.logo)?has_content> +

+ Logo +

+ + +
+

+ <#nested "header"> +

+ +
+
+
+

Application login area

+ <#nested "form"> +
+ + <#if error?has_content> + + + +
+

Info area

+ <#nested "info"> +
+
+
+ + <#if template.themeConfig['displayPoweredBy']> +

+ ${rb.getString('poweredByKeycloak')} +

+ +
+ + <#nested "content"> + + + + \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/verify-email.ftl b/forms/src/main/resources/META-INF/resources/forms/verify-email.ftl deleted file mode 100644 index a23e6c47cb..0000000000 --- a/forms/src/main/resources/META-INF/resources/forms/verify-email.ftl +++ /dev/null @@ -1,6 +0,0 @@ - - - -Please verify your email address - - \ No newline at end of file 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 8da383a699..cdf52d84d6 100644 --- a/services/src/main/java/org/keycloak/services/email/EmailSender.java +++ b/services/src/main/java/org/keycloak/services/email/EmailSender.java @@ -23,9 +23,8 @@ package org.keycloak.services.email; import java.net.URI; import java.util.Map.Entry; -import java.util.concurrent.TimeUnit; -import java.util.List; import java.util.Properties; +import java.util.concurrent.TimeUnit; import javax.mail.Message; import javax.mail.MessagingException; @@ -38,6 +37,7 @@ import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import org.jboss.resteasy.logging.Logger; +import org.keycloak.services.managers.AccessCodeEntry; import org.keycloak.services.models.RealmModel; import org.keycloak.services.models.UserModel; import org.keycloak.services.resources.AccountService; @@ -78,12 +78,9 @@ public class EmailSender { transport.sendMessage(msg, new InternetAddress[] { new InternetAddress(address) }); } - public void sendEmailVerification(UserModel user, RealmModel realm, String code, UriInfo uriInfo) { - UriBuilder builder = Urls.accountBase(uriInfo.getBaseUri()).path(AccountService.class, "processEmailVerification"); - for (Entry> e : uriInfo.getQueryParameters().entrySet()) { - builder.queryParam(e.getKey(), e.getValue().toArray()); - } - builder.queryParam("code", code); + public void sendEmailVerification(UserModel user, RealmModel realm, AccessCodeEntry accessCode, UriInfo uriInfo) { + UriBuilder builder = Urls.accountBase(uriInfo.getBaseUri()).path(AccountService.class, "emailVerification"); + builder.queryParam("key", accessCode.getId()); URI uri = builder.build(realm.getId()); @@ -99,12 +96,9 @@ public class EmailSender { } } - public void sendPasswordReset(UserModel user, RealmModel realm, String code, UriInfo uriInfo) { + public void sendPasswordReset(UserModel user, RealmModel realm, AccessCodeEntry accessCode, UriInfo uriInfo) { UriBuilder builder = Urls.accountBase(uriInfo.getBaseUri()).path(AccountService.class, "passwordPage"); - for (Entry> e : uriInfo.getQueryParameters().entrySet()) { - builder.queryParam(e.getKey(), e.getValue().toArray()); - } - builder.queryParam("code", code); + builder.queryParam("key", accessCode.getId()); URI uri = builder.build(realm.getId()); diff --git a/services/src/main/java/org/keycloak/services/models/UserModel.java b/services/src/main/java/org/keycloak/services/models/UserModel.java index 9bd370a59b..2e6d77f907 100755 --- a/services/src/main/java/org/keycloak/services/models/UserModel.java +++ b/services/src/main/java/org/keycloak/services/models/UserModel.java @@ -54,6 +54,6 @@ public interface UserModel { void setTotp(boolean totp); public static enum RequiredAction { - VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, RESET_PASSWORD + VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, UPDATE_PASSWORD } } \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index 231aed4dd1..c07213d2ea 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -28,7 +28,6 @@ import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; -import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; @@ -45,7 +44,6 @@ import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.email.EmailSender; import org.keycloak.services.managers.AccessCodeEntry; import org.keycloak.services.managers.AuthenticationManager; -import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.messages.Messages; import org.keycloak.services.models.RealmModel; @@ -54,7 +52,6 @@ import org.keycloak.services.models.UserModel; import org.keycloak.services.models.UserModel.RequiredAction; import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.FormFlows; -import org.keycloak.services.resources.flows.OAuthFlows; import org.keycloak.services.validation.Validation; import org.picketlink.idm.credential.util.TimeBasedOTP; @@ -116,9 +113,8 @@ public class AccountService { accessCodeEntry.getRequiredActions().remove(UserModel.RequiredAction.UPDATE_PROFILE); } - Response response = redirectOauth(user, accessCodeEntry); - if (response != null) { - return response; + if (accessCodeEntry != null) { + return redirectOauth(user, accessCodeEntry); } else { return Flows.forms(realm, request, uriInfo).setUser(user).forwardToAccount(); } @@ -207,34 +203,88 @@ public class AccountService { user.setTotp(true); - Response response = redirectOauth(user, accessCodeEntry); - if (response != null) { - return response; + if (accessCodeEntry != null) { + return redirectOauth(user, accessCodeEntry); } else { return Flows.forms(realm, request, uriInfo).setUser(user).forwardToTotp(); } } - @Path("email-verify") + @Path("password-reset") @GET - public Response processEmailVerification() { - AccessCodeEntry accessCodeEntry = getAccessCodeEntry(RequiredAction.VERIFY_EMAIL); - UserModel user = accessCodeEntry != null ? getUserFromAccessCode(accessCodeEntry) : null; - if (user == null) { - return Response.status(Status.FORBIDDEN).build(); + public Response passwordReset() { + return Flows.forms(realm, request, uriInfo).forwardToPasswordReset(); + } + + @Path("password-reset") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response sendPasswordReset(final MultivaluedMap formData) { + String username = formData.getFirst("username"); + String email = formData.getFirst("email"); + + String scopeParam = uriInfo.getQueryParameters().getFirst("scope"); + String state = uriInfo.getQueryParameters().getFirst("state"); + String redirect = uriInfo.getQueryParameters().getFirst("redirect_uri"); + 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."); } - user.setEmailVerified(true); - user.removeRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL); - if (accessCodeEntry != null) { - accessCodeEntry.getRequiredActions().remove(UserModel.RequiredAction.VERIFY_EMAIL); + UserModel user = realm.getUser(username); + if (user == null || !email.equals(user.getEmail())) { + Flows.forms(realm, request, uriInfo).setError("Invalid username or email") + .forwardToAction(RequiredAction.UPDATE_PASSWORD); } - Response response = redirectOauth(user, accessCodeEntry); - if (response != null) { - return response; + 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()); + + new EmailSender().sendPasswordReset(user, realm, accessCode, uriInfo); + + return Flows.forms(realm, request, uriInfo).forwardToPasswordReset(); + } + + @Path("email-verification") + @GET + public Response emailVerification() { + if (uriInfo.getQueryParameters().containsKey("key")) { + AccessCodeEntry accessCode = tokenManager.getAccessCode(uriInfo.getQueryParameters().getFirst("key")); + if (accessCode == null || accessCode.isExpired() + || !accessCode.getRequiredActions().contains(RequiredAction.VERIFY_EMAIL)) { + return Response.status(Status.FORBIDDEN).build(); + } + + String loginName = accessCode.getUser().getLoginName(); + UserModel user = realm.getUser(loginName); + user.setEmailVerified(true); + user.removeRequiredAction(RequiredAction.VERIFY_EMAIL); + + accessCode.getRequiredActions().remove(RequiredAction.VERIFY_EMAIL); + + return redirectOauth(user, accessCode); } else { - return Flows.forms(realm, request, uriInfo).setUser(user).forwardToVerifyEmail(); + AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.VERIFY_EMAIL); + UserModel user = accessCode != null ? getUserFromAccessCode(accessCode) : null; + if (user == null) { + return Response.status(Status.FORBIDDEN).build(); + } + + new EmailSender().sendEmailVerification(user, realm, accessCode, uriInfo); + + return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(user) + .forwardToAction(RequiredAction.VERIFY_EMAIL); } } @@ -245,17 +295,11 @@ public class AccountService { Set requiredActions = user.getRequiredActions(); if (!requiredActions.isEmpty()) { - return Flows.forms(realm, request, uriInfo).setCode(accessCode.getCode()).setUser(user) + return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(user) .forwardToAction(requiredActions.iterator().next()); } else { - String redirect = uriInfo.getQueryParameters().getFirst("redirect_uri"); - if (redirect != null) { - String state = uriInfo.getQueryParameters().getFirst("state"); - return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode, state, - redirect); - } else { - return null; - } + return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode, + accessCode.getState(), accessCode.getRedirectUri()); } } @@ -263,12 +307,14 @@ public class AccountService { @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response processPasswordUpdate(final MultivaluedMap formData) { - AccessCodeEntry accessCodeEntry = getAccessCodeEntry(RequiredAction.RESET_PASSWORD); - UserModel user = accessCodeEntry != null ? getUserFromAccessCode(accessCodeEntry) : getUserFromAuthManager(); + AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.UPDATE_PASSWORD); + UserModel user = accessCode != null ? getUserFromAccessCode(accessCode) : getUserFromAuthManager(); if (user == null) { return Response.status(Status.FORBIDDEN).build(); } + boolean loginAction = accessCode != null; + FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user); String password = formData.getFirst("password"); @@ -283,7 +329,7 @@ public class AccountService { error = Messages.INVALID_PASSWORD_CONFIRM; } - if (accessCodeEntry == null) { + if (!loginAction) { if (Validation.isEmpty(password)) { error = Messages.MISSING_PASSWORD; } else if (!realm.validatePassword(user, password)) { @@ -301,15 +347,16 @@ public class AccountService { realm.updateCredential(user, credentials); - user.removeRequiredAction(RequiredAction.RESET_PASSWORD); - if (accessCodeEntry != null) { - accessCodeEntry.getRequiredActions().remove(UserModel.RequiredAction.RESET_PASSWORD); + user.removeRequiredAction(RequiredAction.UPDATE_PASSWORD); + if (accessCode != null) { + accessCode.getRequiredActions().remove(UserModel.RequiredAction.UPDATE_PASSWORD); } - authManager.expireIdentityCookie(realm, uriInfo); - new ResourceAdminManager().singleLogOut(realm, user.getLoginName()); - - return Flows.forms(realm, request, uriInfo).forwardToLogin(); + if (accessCode != null) { + return redirectOauth(user, accessCode); + } else { + return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword(); + } } @Path("") @@ -348,61 +395,21 @@ public class AccountService { @Path("password") @GET public Response passwordPage() { - UserModel user = getUserFromAuthManager(); + if (uriInfo.getQueryParameters().containsKey("key")) { + AccessCodeEntry accessCode = tokenManager.getAccessCode(uriInfo.getQueryParameters().getFirst("key")); + if (accessCode == null || accessCode.isExpired() + || !accessCode.getRequiredActions().contains(RequiredAction.UPDATE_PASSWORD)) { + return Response.status(Status.FORBIDDEN).build(); + } - // TODO Remove when we have a separate login-reset-password page - if (user == null) { - AccessCodeEntry accessCodeEntry = getAccessCodeEntry(RequiredAction.RESET_PASSWORD); - user = accessCodeEntry != null ? getUserFromAccessCode(accessCodeEntry) : null; - } - - if (user != null) { - return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword(); + return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode) + .forwardToAction(RequiredAction.UPDATE_PASSWORD); } else { - return Response.status(Status.FORBIDDEN).build(); + UserModel user = getUserFromAuthManager(); + if (user == null) { + return Response.status(Status.FORBIDDEN).build(); + } + return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword(); } } - - @Path("password-reset") - @GET - public Response resetPassword(@QueryParam("username") final String username, - @QueryParam("client_id") final String clientId, @QueryParam("scope") final String scopeParam, - @QueryParam("state") final String state, @QueryParam("redirect_uri") final String redirect) { - - OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager); - - if (!realm.isEnabled()) { - return oauth.forwardToSecurityFailure("Realm not enabled."); - } - if (!realm.isResetPasswordAllowed()) { - return oauth.forwardToSecurityFailure("Password reset not permitted, contact admin."); - } - - UserModel client = realm.getUser(clientId); - if (client == null) { - return oauth.forwardToSecurityFailure("Unknown login requester."); - } - if (!client.isEnabled()) { - return oauth.forwardToSecurityFailure("Login requester not enabled."); - } - - // String username = formData.getFirst("username"); - UserModel user = realm.getUser(username); - - Set requiredActions = new HashSet(user.getRequiredActions()); - requiredActions.add(RequiredAction.RESET_PASSWORD); - - AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user); - accessCode.setRequiredActions(requiredActions); - accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction()); - - if (user.getEmail() == null) { - return oauth.forwardToSecurityFailure("Email address not set, contact admin"); - } - - new EmailSender().sendPasswordReset(user, realm, accessCode.getCode(), uriInfo); - // TODO Add info message - return Flows.forms(realm, request, uriInfo).forwardToLogin(); - } - } 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 c33be7456f..a0996ab2b2 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 @@ -28,6 +28,7 @@ import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.ResteasyUriInfo; import org.keycloak.services.FormService; import org.keycloak.services.email.EmailSender; +import org.keycloak.services.managers.AccessCodeEntry; import org.keycloak.services.models.RealmModel; import org.keycloak.services.models.UserModel; import org.keycloak.services.models.UserModel.RequiredAction; @@ -60,7 +61,7 @@ public class FormFlows { private UserModel userModel; private boolean socialRegistration; - private String code; + private AccessCodeEntry accessCode; private UriInfo uriInfo; FormFlows(RealmModel realm, HttpRequest request, UriInfo uriInfo) { @@ -72,15 +73,16 @@ public class FormFlows { public Response forwardToAction(RequiredAction action) { switch (action) { case CONFIGURE_TOTP: - return forwardToTotp(); + return forwardToForm(Pages.LOGIN_CONFIG_TOTP); case UPDATE_PROFILE: - return forwardToAccount(); - case RESET_PASSWORD: - return forwardToPassword(); + return forwardToForm(Pages.LOGIN_UPDATE_PROFILE); + case UPDATE_PASSWORD: + return forwardToForm(Pages.LOGIN_UPDATE_PASSWORD); case VERIFY_EMAIL: - return forwardToVerifyEmail(); + new EmailSender().sendEmailVerification(userModel, realm, accessCode, uriInfo); + return forwardToForm(Pages.LOGIN_VERIFY_EMAIL); default: - return null; // TODO + return Response.serverError().build(); } } @@ -107,8 +109,8 @@ public class FormFlows { uriBuilder.replaceQueryParam(k, queryParameterMap.get(k).toArray()); } - if (code != null){ - uriBuilder.queryParam(CODE, code); + if (accessCode != null) { + uriBuilder.queryParam(CODE, accessCode.getCode()); } URI baseURI = uriBuilder.build(); @@ -135,6 +137,10 @@ public class FormFlows { return forwardToForm(Pages.LOGIN); } + public Response forwardToPasswordReset() { + return forwardToForm(Pages.LOGIN_RESET_PASSWORD); + } + public Response forwardToLoginTotp() { return forwardToForm(Pages.LOGIN_TOTP); } @@ -155,13 +161,8 @@ public class FormFlows { return forwardToForm(Pages.TOTP); } - public Response forwardToVerifyEmail() { - new EmailSender().sendEmailVerification(userModel, realm, code, uriInfo); - return forwardToForm(Pages.VERIFY_EMAIL); - } - - public FormFlows setCode(String code) { - this.code = code; + public FormFlows setAccessCode(AccessCodeEntry accessCode) { + this.accessCode = accessCode; return this; } diff --git a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java index cc76bd08ca..25ba3d86ea 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java @@ -97,7 +97,7 @@ public class OAuthFlows { if (!requiredActions.isEmpty()) { accessCode.setRequiredActions(new HashSet(requiredActions)); accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction()); - return Flows.forms(realm, request, uriInfo).setCode(accessCode.getCode()).setUser(user) + return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(user) .forwardToAction(user.getRequiredActions().iterator().next()); } 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 123b1dd6e8..a5f913d380 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 @@ -34,10 +34,18 @@ public class Pages { public final static String LOGIN_TOTP = "/forms/login-totp.ftl"; + public final static String LOGIN_CONFIG_TOTP = "/forms/login-config-totp.ftl"; + + public final static String LOGIN_VERIFY_EMAIL = "/forms/login-verify-email.ftl"; + public final static String OAUTH_GRANT = "/saas/oauthGrantForm.jsp"; public final static String PASSWORD = "/forms/password.ftl"; + public final static String LOGIN_RESET_PASSWORD = "/forms/login-reset-password.ftl"; + + public final static String LOGIN_UPDATE_PASSWORD = "/forms/login-update-password.ftl"; + public final static String REGISTER = "/forms/register.ftl"; public final static String SECURITY_FAILURE = "/saas/securityFailure.jsp"; @@ -46,6 +54,6 @@ public class Pages { public final static String TOTP = "/forms/totp.ftl"; - public final static String VERIFY_EMAIL = "/forms/verify-email.ftl"; + public final static String LOGIN_UPDATE_PROFILE = "/forms/login-update-profile.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 b797ff41c6..1da6a61ef3 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 @@ -55,6 +55,14 @@ public class Urls { return accountBase(baseUri).path(AccountService.class, "totpPage").build(realmId); } + public static URI accountEmailVerification(URI baseUri, String realmId) { + return accountBase(baseUri).path(AccountService.class, "emailVerification").build(realmId); + } + + public static URI accountPasswordReset(URI baseUri, String realmId) { + return accountBase(baseUri).path(AccountService.class, "passwordReset").build(realmId); + } + private static UriBuilder realmBase(URI baseUri) { return UriBuilder.fromUri(baseUri).path(RealmsResource.class); } diff --git a/testsuite/src/test/java/org/keycloak/testsuite/AccountTest.java b/testsuite/src/test/java/org/keycloak/testsuite/AccountTest.java index 9fef302766..6321031ba4 100644 --- a/testsuite/src/test/java/org/keycloak/testsuite/AccountTest.java +++ b/testsuite/src/test/java/org/keycloak/testsuite/AccountTest.java @@ -51,6 +51,8 @@ public class AccountTest extends AbstractDroneTest { changePasswordPage.changePassword("password", "new-password", "new-password"); appPage.open(); + Assert.assertTrue(appPage.isCurrent()); + appPage.logout(); Assert.assertTrue(loginPage.isCurrent()); diff --git a/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionEmailVerificationTest.java b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionEmailVerificationTest.java index 6ca1a0121c..134a6b194d 100644 --- a/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionEmailVerificationTest.java +++ b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionEmailVerificationTest.java @@ -22,6 +22,8 @@ package org.keycloak.testsuite; import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; @@ -94,12 +96,17 @@ public class RequiredActionEmailVerificationTest { loginPage.register(); registerPage.register("name", "email", "verifyEmail", "password", "password"); - Assert.assertTrue(browser.getPageSource().contains("Please verify your email address")); + Assert.assertTrue(browser.getPageSource().contains("Verify email")); MimeMessage message = greenMail.getReceivedMessages()[0]; String body = (String) message.getContent(); - String verificationUrl = body.split("\n")[0]; + + Pattern p = Pattern.compile("(?s).*(http://[^\\s]*).*"); + Matcher m = p.matcher(body); + m.matches(); + + String verificationUrl = m.group(1); browser.navigate().to(verificationUrl.trim()); diff --git a/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionMultipleActionsTest.java b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionMultipleActionsTest.java new file mode 100644 index 0000000000..1964ec7b0f --- /dev/null +++ b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionMultipleActionsTest.java @@ -0,0 +1,94 @@ +/* + * 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; + +import java.net.MalformedURLException; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.drone.api.annotation.Drone; +import org.jboss.arquillian.graphene.page.Page; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.pages.RegisterPage; +import org.keycloak.testsuite.pages.TotpPage; +import org.openqa.selenium.WebDriver; +import org.picketlink.idm.credential.util.TimeBasedOTP; + +/** + * @author Stian Thorgersen + */ +@RunWith(Arquillian.class) +public class RequiredActionMultipleActionsTest { + + @Deployment(name = "app", testable = false, order = 2) + public static WebArchive appDeployment() { + return Deployments.appDeployment(); + } + + @Deployment(name = "auth-server", testable = false, order = 1) + public static WebArchive deployment() { + return Deployments.deployment().addAsResource("testrealm-totp.json", "META-INF/testrealm.json"); + } + + @Page + protected AppPage appPage; + + @Drone + protected WebDriver browser; + + @Page + protected TotpPage totpPage; + + @Page + protected LoginPage loginPage; + + @Page + protected RegisterPage registerPage; + + protected TimeBasedOTP totp; + + @Before + public void before() throws MalformedURLException { + totp = new TimeBasedOTP(); + } + + @After + public void after() { + appPage.open(); + if (appPage.isCurrent()) { + appPage.logout(); + } + } + + @Test + public void setupTotp() { + Assert.fail("Not implemented"); + } + +} diff --git a/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionResetPasswordTest.java b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionResetPasswordTest.java new file mode 100644 index 0000000000..0f369a78f0 --- /dev/null +++ b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionResetPasswordTest.java @@ -0,0 +1,79 @@ +/* + * 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; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.graphene.page.Page; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; + +/** + * @author Stian Thorgersen + */ +@RunWith(Arquillian.class) +public class RequiredActionResetPasswordTest extends AbstractDroneTest { + + @Deployment(name = "properties", testable = false, order = 1) + public static WebArchive propertiesDeployment() { + return ShrinkWrap.create(WebArchive.class, "properties.war").addClass(SystemPropertiesSetter.class) + .addAsWebInfResource("web-properties-email-verfication.xml", "web.xml"); + } + + @Rule + public GreenMailRule greenMail = new GreenMailRule(); + + @Page + protected LoginPasswordUpdatePage changePasswordPage; + + @Test + public void tempPassword() { + appPage.open(); + + Assert.assertTrue(loginPage.isCurrent()); + + loginPage.login("reset@pass.com", "temp-password"); + + Assert.assertTrue(changePasswordPage.isCurrent()); + + changePasswordPage.changePassword("new-password", "new-password"); + + Assert.assertTrue(appPage.isCurrent()); + Assert.assertEquals("reset@pass.com", appPage.getUser()); + + appPage.logout(); + appPage.open(); + + Assert.assertTrue(loginPage.isCurrent()); + + loginPage.login("reset@pass.com", "new-password"); + + Assert.assertTrue(appPage.isCurrent()); + Assert.assertEquals("reset@pass.com", appPage.getUser()); + } + +} diff --git a/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionTotpSetupTest.java b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionTotpSetupTest.java index 1742bcbb76..0d667eec03 100644 --- a/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionTotpSetupTest.java +++ b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionTotpSetupTest.java @@ -34,9 +34,9 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.LoginConfigTotpPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.RegisterPage; -import org.keycloak.testsuite.pages.TotpPage; import org.openqa.selenium.WebDriver; import org.picketlink.idm.credential.util.TimeBasedOTP; @@ -63,7 +63,7 @@ public class RequiredActionTotpSetupTest { protected WebDriver browser; @Page - protected TotpPage totpPage; + protected LoginConfigTotpPage totpPage; @Page protected LoginPage loginPage; diff --git a/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionUpdateProfileTest.java b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionUpdateProfileTest.java new file mode 100644 index 0000000000..74ae20a47e --- /dev/null +++ b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionUpdateProfileTest.java @@ -0,0 +1,69 @@ +/* + * 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; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.graphene.page.Page; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.keycloak.testsuite.pages.LoginUpdateProfilePage; + +/** + * @author Stian Thorgersen + */ +@RunWith(Arquillian.class) +public class RequiredActionUpdateProfileTest extends AbstractDroneTest { + + @Deployment(name = "properties", testable = false, order = 1) + public static WebArchive propertiesDeployment() { + return ShrinkWrap.create(WebArchive.class, "properties.war").addClass(SystemPropertiesSetter.class) + .addAsWebInfResource("web-properties-email-verfication.xml", "web.xml"); + } + + @Rule + public GreenMailRule greenMail = new GreenMailRule(); + + @Page + protected LoginUpdateProfilePage updateProfilePage; + + @Test + public void updateProfile() { + appPage.open(); + + Assert.assertTrue(loginPage.isCurrent()); + + loginPage.login("updateprof@pass.com", "password"); + + Assert.assertTrue(updateProfilePage.isCurrent()); + + updateProfilePage.update("New first", "New last", "new@email.com"); + + Assert.assertTrue(appPage.isCurrent()); + Assert.assertEquals("updateprof@pass.com", appPage.getUser()); + } + +} diff --git a/testsuite/src/test/java/org/keycloak/testsuite/ResetPasswordTest.java b/testsuite/src/test/java/org/keycloak/testsuite/ResetPasswordTest.java index 22b0ab70fd..755061e50e 100644 --- a/testsuite/src/test/java/org/keycloak/testsuite/ResetPasswordTest.java +++ b/testsuite/src/test/java/org/keycloak/testsuite/ResetPasswordTest.java @@ -35,7 +35,8 @@ import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.keycloak.testsuite.pages.ChangePasswordPage; +import org.keycloak.testsuite.pages.LoginPasswordResetPage; +import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; /** * @author Stian Thorgersen @@ -53,7 +54,10 @@ public class ResetPasswordTest extends AbstractDroneTest { public GreenMailRule greenMail = new GreenMailRule(); @Page - protected ChangePasswordPage changePasswordPage; + protected LoginPasswordResetPage resetPasswordPage; + + @Page + protected LoginPasswordUpdatePage updatePasswordPage; @Test public void resetPassword() throws IOException, MessagingException { @@ -61,12 +65,13 @@ public class ResetPasswordTest extends AbstractDroneTest { Assert.assertTrue(loginPage.isCurrent()); - // TODO Replace with clicking reset password link when added - String url = browser.getCurrentUrl(); - url = url.replace("tokens/login", "account/password-reset"); - url = url + "&username=bburke@redhat.com"; + loginPage.resetPassword(); - browser.navigate().to(url); + Assert.assertTrue(resetPasswordPage.isCurrent()); + + resetPasswordPage.changePassword("bburke@redhat.com", "bburke@redhat.com"); + + Assert.assertTrue(resetPasswordPage.isCurrent()); Assert.assertEquals(1, greenMail.getReceivedMessages().length); @@ -77,38 +82,22 @@ public class ResetPasswordTest extends AbstractDroneTest { browser.navigate().to(changePasswordUrl.trim()); - changePasswordPage.changePassword("new-password", "new-password"); + Assert.assertTrue(updatePasswordPage.isCurrent()); + + updatePasswordPage.changePassword("new-password", "new-password"); + + Assert.assertTrue(appPage.isCurrent()); + Assert.assertEquals("bburke@redhat.com", appPage.getUser()); + + appPage.logout(); + appPage.open(); Assert.assertTrue(loginPage.isCurrent()); - loginPage.login("bburke@redhat.com", "password"); - Assert.assertTrue(loginPage.isCurrent()); - Assert.assertEquals("Invalid username or password", loginPage.getError()); - loginPage.login("bburke@redhat.com", "new-password"); Assert.assertTrue(appPage.isCurrent()); Assert.assertEquals("bburke@redhat.com", appPage.getUser()); } - @Test - public void tempPassword() { - appPage.open(); - - Assert.assertTrue(loginPage.isCurrent()); - - loginPage.login("reset@pass.com", "temp-password"); - - Assert.assertTrue(changePasswordPage.isCurrent()); - - changePasswordPage.changePassword("new-password", "new-password"); - - Assert.assertTrue(loginPage.isCurrent()); - - loginPage.login("reset@pass.com", "new-password"); - - Assert.assertTrue(appPage.isCurrent()); - Assert.assertEquals("reset@pass.com", appPage.getUser()); - } - } diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/ChangePasswordPage.java b/testsuite/src/test/java/org/keycloak/testsuite/pages/ChangePasswordPage.java index 1e0185fc19..3b17f9aa93 100644 --- a/testsuite/src/test/java/org/keycloak/testsuite/pages/ChangePasswordPage.java +++ b/testsuite/src/test/java/org/keycloak/testsuite/pages/ChangePasswordPage.java @@ -25,13 +25,6 @@ public class ChangePasswordPage { @FindBy(css = "input[type=\"submit\"]") private WebElement submitButton; - public void changePassword(String newPassword, String passwordConfirm) { - newPasswordInput.sendKeys(newPassword); - passwordConfirmInput.sendKeys(passwordConfirm); - - submitButton.click(); - } - public void changePassword(String password, String newPassword, String passwordConfirm) { passwordInput.sendKeys(password); newPasswordInput.sendKeys(newPassword); diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginConfigTotpPage.java b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginConfigTotpPage.java new file mode 100644 index 0000000000..1c7c062ad3 --- /dev/null +++ b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginConfigTotpPage.java @@ -0,0 +1,42 @@ +package org.keycloak.testsuite.pages; + +import org.jboss.arquillian.drone.api.annotation.Drone; +import org.keycloak.testsuite.Constants; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +public class LoginConfigTotpPage { + + private static String PATH = Constants.AUTH_SERVER_ROOT + "/rest/realms/demo/account/totp"; + + @Drone + private WebDriver browser; + + @FindBy(id = "totpSecret") + private WebElement totpSecret; + + @FindBy(id = "totp") + private WebElement totpInput; + + @FindBy(css = "input[type=\"submit\"]") + private WebElement submitButton; + + public void configure(String totp) { + totpInput.sendKeys(totp); + submitButton.click(); + } + + public String getTotpSecret() { + return totpSecret.getAttribute("value"); + } + + public boolean isCurrent() { + return browser.getTitle().equals("Config TOTP"); + } + + public void open() { + browser.navigate().to(PATH); + } + +} diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPage.java b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPage.java index 0e4a5496c6..c639bdd433 100644 --- a/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPage.java +++ b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPage.java @@ -22,6 +22,9 @@ public class LoginPage { @FindBy(linkText = "Register") private WebElement registerLink; + @FindBy(linkText = "Reset password") + private WebElement resetPasswordLink; + @FindBy(id = "loginError") private WebElement loginErrorMessage; @@ -47,4 +50,8 @@ public class LoginPage { registerLink.click(); } + public void resetPassword() { + resetPasswordLink.click(); + } + } diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java new file mode 100644 index 0000000000..7bd983670c --- /dev/null +++ b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java @@ -0,0 +1,40 @@ +package org.keycloak.testsuite.pages; + +import org.jboss.arquillian.drone.api.annotation.Drone; +import org.keycloak.testsuite.Constants; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +public class LoginPasswordResetPage { + + private static String PATH = Constants.AUTH_SERVER_ROOT + "/rest/realms/demo/account/password"; + + @Drone + private WebDriver browser; + + @FindBy(id = "username") + private WebElement usernameInput; + + @FindBy(id = "email") + private WebElement emailInput; + + @FindBy(css = "input[type=\"submit\"]") + private WebElement submitButton; + + public void changePassword(String username, String email) { + usernameInput.sendKeys(username); + emailInput.sendKeys(email); + + submitButton.click(); + } + + public boolean isCurrent() { + return browser.getTitle().equals("Reset password"); + } + + public void open() { + browser.navigate().to(PATH); + } + +} diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java new file mode 100644 index 0000000000..2a19f0c7f6 --- /dev/null +++ b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java @@ -0,0 +1,40 @@ +package org.keycloak.testsuite.pages; + +import org.jboss.arquillian.drone.api.annotation.Drone; +import org.keycloak.testsuite.Constants; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +public class LoginPasswordUpdatePage { + + private static String PATH = Constants.AUTH_SERVER_ROOT + "/rest/realms/demo/account/password"; + + @Drone + private WebDriver browser; + + @FindBy(id = "password-new") + private WebElement newPasswordInput; + + @FindBy(id = "password-confirm") + private WebElement passwordConfirmInput; + + @FindBy(css = "input[type=\"submit\"]") + private WebElement submitButton; + + public void changePassword(String newPassword, String passwordConfirm) { + newPasswordInput.sendKeys(newPassword); + passwordConfirmInput.sendKeys(passwordConfirm); + + submitButton.click(); + } + + public boolean isCurrent() { + return browser.getTitle().equals("Update password"); + } + + public void open() { + browser.navigate().to(PATH); + } + +} diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java new file mode 100644 index 0000000000..2f3796108e --- /dev/null +++ b/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java @@ -0,0 +1,44 @@ +package org.keycloak.testsuite.pages; + +import org.jboss.arquillian.drone.api.annotation.Drone; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +public class LoginUpdateProfilePage { + + @Drone + private WebDriver browser; + + @FindBy(id = "firstName") + private WebElement firstNameInput; + + @FindBy(id = "lastName") + private WebElement lastNameInput; + + @FindBy(id = "email") + private WebElement emailInput; + + @FindBy(css = "input[type=\"submit\"]") + private WebElement submitButton; + + @FindBy(id = "loginError") + private WebElement loginErrorMessage; + + public void update(String firstName, String lastName, String email) { + firstNameInput.sendKeys(firstName); + lastNameInput.sendKeys(lastName); + emailInput.sendKeys(email); + + submitButton.click(); + } + + public String getError() { + return loginErrorMessage != null ? loginErrorMessage.getText() : null; + } + + public boolean isCurrent() { + return browser.getTitle().equals("Update profile"); + } + +} diff --git a/testsuite/src/test/resources/testrealm.json b/testsuite/src/test/resources/testrealm.json index 0ea28100f2..72a8d61f83 100755 --- a/testsuite/src/test/resources/testrealm.json +++ b/testsuite/src/test/resources/testrealm.json @@ -27,13 +27,23 @@ { "username" : "reset@pass.com", "enabled": true, - "requiredActions" : [ "RESET_PASSWORD" ], + "requiredActions" : [ "UPDATE_PASSWORD" ], "email" : "reset@pass.com", "credentials" : [ { "type" : "password", "value" : "temp-password" } ] }, + { + "username" : "updateprof@pass.com", + "enabled": true, + "requiredActions" : [ "UPDATE_PROFILE" ], + "email" : "reset@pass.com", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ] + }, { "username" : "third-party", "enabled": true, @@ -62,6 +72,10 @@ "username": "reset@pass.com", "roles": ["user"] }, + { + "username": "updateprof@pass.com", + "roles": ["user"] + }, { "username": "third-party", "roles": ["KEYCLOAK_IDENTITY_REQUESTER"]