KEYCLOAK-2806
This commit is contained in:
parent
9b18601102
commit
5f07fa8057
6 changed files with 111 additions and 13 deletions
|
@ -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")
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue