KEYCLOAK-2806

This commit is contained in:
Bill Burke 2016-12-08 16:28:22 -05:00
parent 9b18601102
commit 5f07fa8057
6 changed files with 111 additions and 13 deletions

View file

@ -98,13 +98,32 @@ public interface UserResource {
@Deprecated @Deprecated
public void resetPasswordEmail(@QueryParam("client_id") String clientId); 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 @PUT
@Path("execute-actions-email") @Path("execute-actions-email")
public void executeActionsEmail(List<String> actions); public void executeActionsEmail(List<String> 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 @PUT
@Path("execute-actions-email") @Path("execute-actions-email")
public void executeActionsEmail(@QueryParam("client_id") String clientId, List<String> actions); public void executeActionsEmail(@QueryParam("client_id") String clientId, @QueryParam("redirect_uri") String redirectUri, List<String> actions);
@PUT @PUT
@Path("send-verify-email") @Path("send-verify-email")

View file

@ -77,6 +77,7 @@ import java.util.Set;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class AuthenticationManager { 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"; public static final String END_AFTER_REQUIRED_ACTIONS = "END_AFTER_REQUIRED_ACTIONS";
// userSession note with authTime (time when authentication flow including requiredActions was finished) // 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) { 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) { if (clientSession.getNote(END_AFTER_REQUIRED_ACTIONS) != null) {
Response response = session.getProvider(LoginFormsProvider.class) LoginFormsProvider infoPage = session.getProvider(LoginFormsProvider.class)
.setAttribute("skipLink", true) .setSuccess(Messages.ACCOUNT_UPDATED);
.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(); .createInfoPage();
session.sessions().removeUserSession(session.getContext().getRealm(), userSession); session.sessions().removeUserSession(session.getContext().getRealm(), userSession);
return response; return response;

View file

@ -836,8 +836,9 @@ public class UsersResource {
* Send a update account email to the user * Send a update account email to the user
* *
* An email contains a link the user can click to perform a set of required actions. * 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 * The redirectUri and clientId parameters are optional. If no redirect is given, then there will
* redirect is the account client. * 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 id User is
* @param redirectUri Redirect uri * @param redirectUri Redirect uri
@ -867,6 +868,10 @@ public class UsersResource {
for (String action : actions) { for (String action : actions) {
clientSession.addRequiredAction(action); clientSession.addRequiredAction(action);
} }
if (redirectUri != null) {
clientSession.setNote(AuthenticationManager.SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS, "true");
}
ClientSessionCode accessCode = new ClientSessionCode(session, realm, clientSession); ClientSessionCode accessCode = new ClientSessionCode(session, realm, clientSession);
accessCode.setAction(ClientSessionModel.Action.EXECUTE_ACTIONS.name()); accessCode.setAction(ClientSessionModel.Action.EXECUTE_ACTIONS.name());
@ -933,15 +938,13 @@ public class UsersResource {
ErrorResponse.error(clientId + " not enabled", Response.Status.BAD_REQUEST)); ErrorResponse.error(clientId + " not enabled", Response.Status.BAD_REQUEST));
} }
String redirect; String redirect = null;
if (redirectUri != null) { if (redirectUri != null) {
redirect = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri, realm, client); redirect = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri, realm, client);
if (redirect == null) { if (redirect == null) {
throw new WebApplicationException( throw new WebApplicationException(
ErrorResponse.error("Invalid redirect uri.", Response.Status.BAD_REQUEST)); ErrorResponse.error("Invalid redirect uri.", Response.Status.BAD_REQUEST));
} }
} else {
redirect = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString();
} }

View file

@ -83,7 +83,6 @@ public abstract class AbstractAdminTest extends TestRealmKeycloakTest {
} }
// old testsuite expects this realm to be removed at the end of the test // old testsuite expects this realm to be removed at the end of the test
// not sure if it really matters
@After @After
public void after() { public void after() {
for (RealmRepresentation r : adminClient.realms().findAll()) { for (RealmRepresentation r : adminClient.realms().findAll()) {

View file

@ -33,6 +33,7 @@ import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType; import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.ErrorRepresentation; import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation;
@ -501,7 +502,7 @@ public class UserTest extends AbstractAdminTest {
userRep.setEnabled(true); userRep.setEnabled(true);
updateUser(user, userRep); updateUser(user, userRep);
user.executeActionsEmail("invalidClientId", actions); user.executeActionsEmail("invalidClientId", "invalidUri", actions);
fail("Expected failure"); fail("Expected failure");
} catch (ClientErrorException e) { } catch (ClientErrorException e) {
assertEquals(400, e.getResponse().getStatus()); assertEquals(400, e.getResponse().getStatus());
@ -523,7 +524,7 @@ public class UserTest extends AbstractAdminTest {
UserResource user = realm.users().get(id); UserResource user = realm.users().get(id);
List<String> actions = new LinkedList<>(); List<String> actions = new LinkedList<>();
actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name()); 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); assertAdminEvents.assertEvent(realmId, OperationType.ACTION, AdminEventPaths.userResourcePath(id) + "/execute-actions-email", ResourceType.USER);
Assert.assertEquals(1, greenMail.getReceivedMessages().length); Assert.assertEquals(1, greenMail.getReceivedMessages().length);
@ -545,6 +546,71 @@ public class UserTest extends AbstractAdminTest {
assertEquals("We're sorry...", driver.getTitle()); 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<String> 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 @Test
public void sendVerifyEmail() throws IOException, MessagingException { public void sendVerifyEmail() throws IOException, MessagingException {

View file

@ -9,7 +9,9 @@
<p class="instruction">${message.summary}</p> <p class="instruction">${message.summary}</p>
<#if skipLink??> <#if skipLink??>
<#else> <#else>
<#if client.baseUrl??> <#if pageRedirectUri??>
<p><a href="${pageRedirectUri}">${msg("backToApplication")}</a></p>
<#elseif client.baseUrl??>
<p><a href="${client.baseUrl}">${msg("backToApplication")}</a></p> <p><a href="${client.baseUrl}">${msg("backToApplication")}</a></p>
</#if> </#if>
</#if> </#if>