From 5f07fa8057cbf2d8fb1060bfbf60fa20e91d81ea Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Thu, 8 Dec 2016 16:28:22 -0500 Subject: [PATCH] KEYCLOAK-2806 --- .../admin/client/resource/UserResource.java | 21 +++++- .../managers/AuthenticationManager.java | 15 +++- .../resources/admin/UsersResource.java | 13 ++-- .../testsuite/admin/AbstractAdminTest.java | 1 - .../keycloak/testsuite/admin/UserTest.java | 70 ++++++++++++++++++- .../main/resources/theme/base/login/info.ftl | 4 +- 6 files changed, 111 insertions(+), 13 deletions(-) diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java index e871313cfc..9cb090bb91 100755 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java @@ -98,13 +98,32 @@ public interface UserResource { @Deprecated public void resetPasswordEmail(@QueryParam("client_id") String clientId); + /** + * Sends an email to the user with a link within it. If they click on the link they will be asked to perform some actions + * i.e. reset password, update profile, etc. + * + * + * @param actions + */ @PUT @Path("execute-actions-email") public void executeActionsEmail(List actions); + /** + * Sends an email to the user with a link within it. If they click on the link they will be asked to perform some actions + * i.e. reset password, update profile, etc. + * + * If redirectUri is not null, then you must specify a client id. This will set the URI you want the flow to link + * to after the email link is clicked and actions completed. If both parameters are null, then no page is linked to + * at the end of the flow. + * + * @param clientId + * @param redirectUri + * @param actions + */ @PUT @Path("execute-actions-email") - public void executeActionsEmail(@QueryParam("client_id") String clientId, List actions); + public void executeActionsEmail(@QueryParam("client_id") String clientId, @QueryParam("redirect_uri") String redirectUri, List actions); @PUT @Path("send-verify-email") diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index b3f2638e99..04dfadaccb 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -77,6 +77,7 @@ import java.util.Set; * @version $Revision: 1 $ */ public class AuthenticationManager { + public static final String SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS= "SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS"; public static final String END_AFTER_REQUIRED_ACTIONS = "END_AFTER_REQUIRED_ACTIONS"; // userSession note with authTime (time when authentication flow including requiredActions was finished) @@ -469,9 +470,17 @@ public class AuthenticationManager { public static Response finishedRequiredActions(KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession, ClientConnection clientConnection, HttpRequest request, UriInfo uriInfo, EventBuilder event) { if (clientSession.getNote(END_AFTER_REQUIRED_ACTIONS) != null) { - Response response = session.getProvider(LoginFormsProvider.class) - .setAttribute("skipLink", true) - .setSuccess(Messages.ACCOUNT_UPDATED) + LoginFormsProvider infoPage = session.getProvider(LoginFormsProvider.class) + .setSuccess(Messages.ACCOUNT_UPDATED); + if (clientSession.getNote(SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS) != null) { + if (clientSession.getRedirectUri() != null) { + infoPage.setAttribute("pageRedirectUri", clientSession.getRedirectUri()); + } + + } else { + infoPage.setAttribute("skipLink", true); + } + Response response = infoPage .createInfoPage(); session.sessions().removeUserSession(session.getContext().getRealm(), userSession); return response; diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index 7a8964f6da..c866ac9f62 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -836,8 +836,9 @@ public class UsersResource { * Send a update account email to the user * * An email contains a link the user can click to perform a set of required actions. - * The redirectUri and clientId parameters are optional. The default for the - * redirect is the account client. + * The redirectUri and clientId parameters are optional. If no redirect is given, then there will + * be no link back to click after actions have completed. Redirect uri must be a valid uri for the + * particular clientId. * * @param id User is * @param redirectUri Redirect uri @@ -867,6 +868,10 @@ public class UsersResource { for (String action : actions) { clientSession.addRequiredAction(action); } + if (redirectUri != null) { + clientSession.setNote(AuthenticationManager.SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS, "true"); + + } ClientSessionCode accessCode = new ClientSessionCode(session, realm, clientSession); accessCode.setAction(ClientSessionModel.Action.EXECUTE_ACTIONS.name()); @@ -933,15 +938,13 @@ public class UsersResource { ErrorResponse.error(clientId + " not enabled", Response.Status.BAD_REQUEST)); } - String redirect; + String redirect = null; if (redirectUri != null) { redirect = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri, realm, client); if (redirect == null) { throw new WebApplicationException( ErrorResponse.error("Invalid redirect uri.", Response.Status.BAD_REQUEST)); } - } else { - redirect = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AbstractAdminTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AbstractAdminTest.java index b09209d5fe..bf98b98154 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AbstractAdminTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AbstractAdminTest.java @@ -83,7 +83,6 @@ public abstract class AbstractAdminTest extends TestRealmKeycloakTest { } // old testsuite expects this realm to be removed at the end of the test - // not sure if it really matters @After public void after() { for (RealmRepresentation r : adminClient.realms().findAll()) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java index 5b4777b9cc..e443191253 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java @@ -33,6 +33,7 @@ import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.models.Constants; import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.ErrorRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation; @@ -501,7 +502,7 @@ public class UserTest extends AbstractAdminTest { userRep.setEnabled(true); updateUser(user, userRep); - user.executeActionsEmail("invalidClientId", actions); + user.executeActionsEmail("invalidClientId", "invalidUri", actions); fail("Expected failure"); } catch (ClientErrorException e) { assertEquals(400, e.getResponse().getStatus()); @@ -523,7 +524,7 @@ public class UserTest extends AbstractAdminTest { UserResource user = realm.users().get(id); List actions = new LinkedList<>(); actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name()); - user.executeActionsEmail("account", actions); + user.executeActionsEmail(actions); assertAdminEvents.assertEvent(realmId, OperationType.ACTION, AdminEventPaths.userResourcePath(id) + "/execute-actions-email", ResourceType.USER); Assert.assertEquals(1, greenMail.getReceivedMessages().length); @@ -545,6 +546,71 @@ public class UserTest extends AbstractAdminTest { assertEquals("We're sorry...", driver.getTitle()); } + @Test + public void sendResetPasswordEmailWithRedirect() throws IOException, MessagingException { + + UserRepresentation userRep = new UserRepresentation(); + userRep.setEnabled(true); + userRep.setUsername("user1"); + userRep.setEmail("user1@test.com"); + + String id = createUser(userRep); + + UserResource user = realm.users().get(id); + + ClientRepresentation client = new ClientRepresentation(); + client.setClientId("myclient"); + client.setRedirectUris(new LinkedList<>()); + client.getRedirectUris().add("http://myclient.com/*"); + client.setName("myclient"); + client.setEnabled(true); + Response response = realm.clients().create(client); + String createdId = ApiUtil.getCreatedId(response); + assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.clientResourcePath(createdId), client, ResourceType.CLIENT); + + + List actions = new LinkedList<>(); + actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name()); + + try { + // test that an invalid redirect uri is rejected. + user.executeActionsEmail("myclient", "http://unregistered-uri.com/", actions); + fail("Expected failure"); + } catch (ClientErrorException e) { + assertEquals(400, e.getResponse().getStatus()); + + ErrorRepresentation error = e.getResponse().readEntity(ErrorRepresentation.class); + Assert.assertEquals("Invalid redirect uri.", error.getErrorMessage()); + } + + + user.executeActionsEmail("myclient", "http://myclient.com/home.html", actions); + assertAdminEvents.assertEvent(realmId, OperationType.ACTION, AdminEventPaths.userResourcePath(id) + "/execute-actions-email", ResourceType.USER); + + Assert.assertEquals(1, greenMail.getReceivedMessages().length); + + MimeMessage message = greenMail.getReceivedMessages()[0]; + + String link = MailUtils.getPasswordResetEmailLink(message); + + driver.navigate().to(link); + + assertTrue(passwordUpdatePage.isCurrent()); + + passwordUpdatePage.changePassword("new-pass", "new-pass"); + + assertEquals("Your account has been updated.", driver.getTitle()); + + String pageSource = driver.getPageSource(); + + // check to make sure the back link is set. + Assert.assertTrue(pageSource.contains("http://myclient.com/home.html")); + + driver.navigate().to(link); + + assertEquals("We're sorry...", driver.getTitle()); + } + @Test public void sendVerifyEmail() throws IOException, MessagingException { diff --git a/themes/src/main/resources/theme/base/login/info.ftl b/themes/src/main/resources/theme/base/login/info.ftl index ca50401f5f..cb228d2a15 100755 --- a/themes/src/main/resources/theme/base/login/info.ftl +++ b/themes/src/main/resources/theme/base/login/info.ftl @@ -9,7 +9,9 @@

${message.summary}

<#if skipLink??> <#else> - <#if client.baseUrl??> + <#if pageRedirectUri??> +

${msg("backToApplication")}

+ <#elseif client.baseUrl??>

${msg("backToApplication")}