Do not allow verifying email from a different account
Closes #14776 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
f476a42d66
commit
8ff9e71eae
11 changed files with 209 additions and 93 deletions
|
@ -94,4 +94,18 @@ For more details, see link:{upgradingguide_link}[{upgradingguide_name}].
|
|||
In this release, the server will render the update profile page when the user is authenticating through a broker for the
|
||||
first time using the `idp-review-user-profile.ftl` template.
|
||||
|
||||
For more details, see link:{upgradingguide_link}[{upgradingguide_name}].
|
||||
For more details, see link:{upgradingguide_link}[{upgradingguide_name}].
|
||||
|
||||
= Performing actions on behalf of another user is not longer possible when the user is already authenticated
|
||||
|
||||
In this release, you can no longer perform actions such as email verification if the user is already authenticated
|
||||
and the action is bound to another user. For instance, a user can not complete the verification email flow if the email link
|
||||
is bound to a different account.
|
||||
|
||||
= Changes to the email verification flow
|
||||
|
||||
In this release, if a user tries to follow the link to verify the email and the email was previously verified, a proper message
|
||||
will be shown.
|
||||
|
||||
In addition to that, a new error (`EMAIL_ALREADY_VERIFIED`) event will be fired to indicate an attempt to verify an already verified email. You can
|
||||
use this event to track possible attempts to hijack user accounts in case the link has leaked or to alert users if they do not recognize the action.
|
|
@ -45,6 +45,7 @@ public interface Errors {
|
|||
String USERNAME_MISSING = "username_missing";
|
||||
String USERNAME_IN_USE = "username_in_use";
|
||||
String EMAIL_IN_USE = "email_in_use";
|
||||
String EMAIL_ALREADY_VERIFIED = "email_already_verified";
|
||||
|
||||
String INVALID_REDIRECT_URI = "invalid_redirect_uri";
|
||||
String INVALID_CODE = "invalid_code";
|
||||
|
|
|
@ -28,13 +28,17 @@ import org.keycloak.models.Constants;
|
|||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserModel.RequiredAction;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriBuilder;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
|
@ -73,10 +77,20 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
|
|||
event.event(EventType.IDENTITY_PROVIDER_LINK_ACCOUNT)
|
||||
.detail(Details.EMAIL, user.getEmail())
|
||||
.detail(Details.IDENTITY_PROVIDER, token.getIdentityProviderAlias())
|
||||
.detail(Details.IDENTITY_PROVIDER_USERNAME, token.getIdentityProviderUsername())
|
||||
.success();
|
||||
.detail(Details.IDENTITY_PROVIDER_USERNAME, token.getIdentityProviderUsername());
|
||||
|
||||
AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
|
||||
|
||||
if (user.isEmailVerified() && !isVerifyEmailActionSet(user, authSession)) {
|
||||
event.user(user).error(Errors.EMAIL_ALREADY_VERIFIED);
|
||||
return session.getProvider(LoginFormsProvider.class)
|
||||
.setAuthenticationSession(session.getContext().getAuthenticationSession())
|
||||
.setInfo(Messages.EMAIL_VERIFIED_ALREADY, user.getEmail())
|
||||
.createInfoPage();
|
||||
}
|
||||
|
||||
event.success();
|
||||
|
||||
if (tokenContext.isAuthenticationSessionFresh()) {
|
||||
token.setOriginalCompoundAuthenticationSessionId(token.getCompoundAuthenticationSessionId());
|
||||
|
||||
|
@ -126,4 +140,8 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
|
|||
return tokenContext.brokerFlow(null, null, authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH));
|
||||
}
|
||||
|
||||
private boolean isVerifyEmailActionSet(UserModel user, AuthenticationSessionModel authSession) {
|
||||
return Stream.concat(user.getRequiredActionsStream(), authSession.getRequiredActions().stream())
|
||||
.anyMatch(RequiredAction.VERIFY_EMAIL.name()::equals);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,8 @@ import org.keycloak.services.messages.Messages;
|
|||
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriBuilder;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
|
@ -66,14 +68,22 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHandler<Ve
|
|||
@Override
|
||||
public Response handleToken(VerifyEmailActionToken token, ActionTokenContext<VerifyEmailActionToken> tokenContext) {
|
||||
UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser();
|
||||
KeycloakSession session = tokenContext.getSession();
|
||||
AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
|
||||
EventBuilder event = tokenContext.getEvent();
|
||||
|
||||
event.event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail());
|
||||
|
||||
AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
|
||||
if (user.isEmailVerified() && !isVerifyEmailActionSet(user, authSession)) {
|
||||
event.user(user).error(Errors.EMAIL_ALREADY_VERIFIED);
|
||||
return session.getProvider(LoginFormsProvider.class)
|
||||
.setAuthenticationSession(authSession)
|
||||
.setInfo(Messages.EMAIL_VERIFIED_ALREADY, user.getEmail())
|
||||
.createInfoPage();
|
||||
}
|
||||
|
||||
final UriInfo uriInfo = tokenContext.getUriInfo();
|
||||
final RealmModel realm = tokenContext.getRealm();
|
||||
final KeycloakSession session = tokenContext.getSession();
|
||||
|
||||
if (tokenContext.isAuthenticationSessionFresh()) {
|
||||
// Update the authentication session in the token
|
||||
|
@ -100,10 +110,10 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHandler<Ve
|
|||
event.success();
|
||||
|
||||
if (token.getCompoundOriginalAuthenticationSessionId() != null) {
|
||||
AuthenticationSessionManager asm = new AuthenticationSessionManager(tokenContext.getSession());
|
||||
AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
|
||||
asm.removeAuthenticationSession(tokenContext.getRealm(), authSession, true);
|
||||
|
||||
return tokenContext.getSession().getProvider(LoginFormsProvider.class)
|
||||
return session.getProvider(LoginFormsProvider.class)
|
||||
.setAuthenticationSession(authSession)
|
||||
.setSuccess(Messages.EMAIL_VERIFIED)
|
||||
.createInfoPage();
|
||||
|
@ -115,4 +125,8 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHandler<Ve
|
|||
return AuthenticationManager.redirectToRequiredActions(session, realm, authSession, uriInfo, nextAction);
|
||||
}
|
||||
|
||||
private boolean isVerifyEmailActionSet(UserModel user, AuthenticationSessionModel authSession) {
|
||||
return Stream.concat(user.getRequiredActionsStream(), authSession.getRequiredActions().stream())
|
||||
.anyMatch(RequiredAction.VERIFY_EMAIL.name()::equals);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,6 +109,7 @@ public class Messages {
|
|||
public static final String LINK_IDP = "linkIdpMessage";
|
||||
|
||||
public static final String EMAIL_VERIFIED = "emailVerifiedMessage";
|
||||
public static final String EMAIL_VERIFIED_ALREADY = "emailVerifiedAlreadyMessage";
|
||||
|
||||
public static final String EMAIL_SENT = "emailSentMessage";
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ import org.keycloak.events.Errors;
|
|||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.exceptions.TokenNotActiveException;
|
||||
import org.keycloak.models.KeycloakContext;
|
||||
import org.keycloak.models.SingleUseObjectKeyModel;
|
||||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
|
@ -524,8 +525,10 @@ public class LoginActionsService {
|
|||
client = realm.getClientByClientId(clientId);
|
||||
}
|
||||
AuthenticationSessionManager authenticationSessionManager = new AuthenticationSessionManager(session);
|
||||
KeycloakContext sessionContext = session.getContext();
|
||||
|
||||
if (client != null) {
|
||||
session.getContext().setClient(client);
|
||||
sessionContext.setClient(client);
|
||||
authSession = authenticationSessionManager.getCurrentAuthenticationSession(realm, client, tabId);
|
||||
}
|
||||
|
||||
|
@ -560,7 +563,7 @@ public class LoginActionsService {
|
|||
.withChecks(
|
||||
// Token introspection checks
|
||||
TokenVerifier.IS_ACTIVE,
|
||||
new TokenVerifier.RealmUrlCheck(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName())),
|
||||
new TokenVerifier.RealmUrlCheck(Urls.realmIssuer(sessionContext.getUri().getBaseUri(), realm.getName())),
|
||||
ACTION_TOKEN_BASIC_CHECKS
|
||||
);
|
||||
|
||||
|
@ -596,22 +599,15 @@ public class LoginActionsService {
|
|||
}
|
||||
|
||||
// Now proceed with the verification and handle the token
|
||||
tokenContext = new ActionTokenContext(session, realm, session.getContext().getUri(), clientConnection, request, event, handler, execution, this::processFlow, this::brokerLoginFlow);
|
||||
tokenContext = new ActionTokenContext(session, realm, sessionContext.getUri(), clientConnection, request, event, handler, execution, this::processFlow, this::brokerLoginFlow);
|
||||
|
||||
try {
|
||||
String tokenAuthSessionCompoundId = handler.getAuthenticationSessionIdFromToken(token, tokenContext, authSession);
|
||||
|
||||
if (tokenAuthSessionCompoundId != null) {
|
||||
// This can happen if the token contains ID but user opens the link in a new browser
|
||||
String sessionId = AuthenticationSessionCompoundId.encoded(tokenAuthSessionCompoundId).getRootSessionId();
|
||||
LoginActionsServiceChecks.checkNotLoggedInYet(tokenContext, authSession, sessionId);
|
||||
}
|
||||
|
||||
if (authSession == null) {
|
||||
authSession = handler.startFreshAuthenticationSession(token, tokenContext);
|
||||
tokenContext.setAuthenticationSession(authSession, true);
|
||||
} else if (tokenAuthSessionCompoundId == null ||
|
||||
! LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, authSession, tokenAuthSessionCompoundId)) {
|
||||
} else if (!LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, authSession, tokenAuthSessionCompoundId)) {
|
||||
// There exists an authentication session but no auth session ID was received in the action token
|
||||
logger.debugf("Authentication session in progress but no authentication session ID was found in action token %s, restarting.", token.getId());
|
||||
authenticationSessionManager.removeAuthenticationSession(realm, authSession, false);
|
||||
|
@ -622,13 +618,14 @@ public class LoginActionsService {
|
|||
processLocaleParam(authSession);
|
||||
}
|
||||
|
||||
sessionContext.setAuthenticationSession(authSession);
|
||||
initLoginEvent(authSession);
|
||||
event.event(handler.eventType());
|
||||
|
||||
LoginActionsServiceChecks.checkIsUserValid(token, tokenContext);
|
||||
LoginActionsServiceChecks.checkIsUserValid(token, tokenContext, event);
|
||||
LoginActionsServiceChecks.checkIsClientValid(token, tokenContext);
|
||||
|
||||
session.getContext().setClient(authSession.getClient());
|
||||
sessionContext.setClient(authSession.getClient());
|
||||
|
||||
TokenVerifier.createWithoutSignature(token)
|
||||
.withChecks(handler.getVerifiers(tokenContext))
|
||||
|
|
|
@ -16,17 +16,18 @@
|
|||
*/
|
||||
package org.keycloak.services.resources;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.keycloak.TokenVerifier.Predicate;
|
||||
import org.keycloak.authentication.AuthenticationProcessor;
|
||||
import org.keycloak.authentication.ExplainedVerificationException;
|
||||
import org.keycloak.authentication.actiontoken.ActionTokenContext;
|
||||
import org.keycloak.authentication.actiontoken.ExplainedTokenVerificationException;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.forms.login.LoginFormsProvider;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.models.SingleUseObjectKeyModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.SingleUseObjectProvider;
|
||||
|
@ -34,7 +35,9 @@ import org.keycloak.models.UserModel;
|
|||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.services.ErrorPageException;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.AuthenticationManager.AuthResult;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
@ -112,38 +115,11 @@ public class LoginActionsServiceChecks {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the authentication session has not yet been converted to user session, in other words
|
||||
* that the user has not yet completed authentication and logged in.
|
||||
*/
|
||||
public static <T extends JsonWebToken> void checkNotLoggedInYet(ActionTokenContext<T> context, AuthenticationSessionModel authSessionFromCookie, String authSessionId) throws VerificationException {
|
||||
if (authSessionId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
UserSessionModel userSession = context.getSession().sessions().getUserSession(context.getRealm(), authSessionId);
|
||||
boolean hasNoRequiredActions =
|
||||
(userSession == null || userSession.getUser().getRequiredActionsStream().count() == 0)
|
||||
&&
|
||||
(authSessionFromCookie == null || authSessionFromCookie.getRequiredActions() == null || authSessionFromCookie.getRequiredActions().isEmpty());
|
||||
|
||||
if (userSession != null && hasNoRequiredActions) {
|
||||
LoginFormsProvider loginForm = context.getSession().getProvider(LoginFormsProvider.class).setAuthenticationSession(context.getAuthenticationSession())
|
||||
.setSuccess(Messages.ALREADY_LOGGED_IN);
|
||||
|
||||
if (context.getSession().getContext().getClient() == null) {
|
||||
loginForm.setAttribute(Constants.SKIP_LINK, true);
|
||||
}
|
||||
|
||||
throw new LoginActionsServiceException(loginForm.createInfoPage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies whether the user given by ID both exists in the current realm. If yes,
|
||||
* it optionally also injects the user using the given function (e.g. into session context).
|
||||
*/
|
||||
public static void checkIsUserValid(KeycloakSession session, RealmModel realm, String userId, Consumer<UserModel> userSetter) throws VerificationException {
|
||||
public static void checkIsUserValid(KeycloakSession session, RealmModel realm, String userId, Consumer<UserModel> userSetter, EventBuilder event) throws VerificationException {
|
||||
UserModel user = userId == null ? null : session.users().getUserById(realm, userId);
|
||||
|
||||
if (user == null) {
|
||||
|
@ -154,6 +130,21 @@ public class LoginActionsServiceChecks {
|
|||
throw new ExplainedVerificationException(Errors.USER_DISABLED, Messages.ACCOUNT_DISABLED);
|
||||
}
|
||||
|
||||
AuthResult authResult = AuthenticationManager.authenticateIdentityCookie(session, realm, true);
|
||||
|
||||
if (authResult != null) {
|
||||
UserSessionModel userSession = authResult.getSession();
|
||||
if (!user.equals(userSession.getUser())) {
|
||||
// do not allow authenticated users performing actions that are bound to other user and fire an event
|
||||
// it might be an attempt to hijack a user account or perform actions on behalf of others
|
||||
// we don't support yet multiple accounts within a same browser session
|
||||
event.detail(Details.EXISTING_USER, userSession.getUser().getId());
|
||||
event.error(Errors.DIFFERENT_USER_AUTHENTICATED);
|
||||
AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession();
|
||||
throw new ErrorPageException(session, authSession, Response.Status.BAD_REQUEST, Messages.DIFFERENT_USER_AUTHENTICATED, userSession.getUser().getUsername());
|
||||
}
|
||||
}
|
||||
|
||||
if (userSetter != null) {
|
||||
userSetter.accept(user);
|
||||
}
|
||||
|
@ -163,9 +154,9 @@ public class LoginActionsServiceChecks {
|
|||
* Verifies whether the user given by ID both exists in the current realm. If yes,
|
||||
* it optionally also injects the user using the given function (e.g. into session context).
|
||||
*/
|
||||
public static <T extends JsonWebToken & SingleUseObjectKeyModel> void checkIsUserValid(T token, ActionTokenContext<T> context) throws VerificationException {
|
||||
public static <T extends JsonWebToken & SingleUseObjectKeyModel> void checkIsUserValid(T token, ActionTokenContext<T> context, EventBuilder event) throws VerificationException {
|
||||
try {
|
||||
checkIsUserValid(context.getSession(), context.getRealm(), token.getUserId(), context.getAuthenticationSession()::setAuthenticatedUser);
|
||||
checkIsUserValid(context.getSession(), context.getRealm(), token.getUserId(), context.getAuthenticationSession()::setAuthenticatedUser, event);
|
||||
} catch (ExplainedVerificationException ex) {
|
||||
throw new ExplainedTokenVerificationException(token, ex);
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.keycloak.events.EventType;
|
|||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel.RequiredAction;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
|
@ -81,6 +82,8 @@ import static org.hamcrest.CoreMatchers.notNullValue;
|
|||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.core.Is.is;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
|
@ -171,7 +174,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
|
||||
MimeMessage message = greenMail.getReceivedMessages()[0];
|
||||
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
AssertEvents.ExpectedEvent emailEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost");
|
||||
EventRepresentation sendEvent = emailEvent.assertEvent();
|
||||
|
@ -209,7 +212,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
EventRepresentation sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).user(userId).detail(Details.USERNAME, "verifyemail").detail("email", "email@mail.com").assertEvent();
|
||||
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
|
||||
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
driver.navigate().to(verificationUrl.trim());
|
||||
|
||||
|
@ -225,6 +228,60 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
events.expectLogin().user(userId).session(mailCodeId).detail(Details.USERNAME, "verifyemail").assertEvent();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyEmailFromAnotherAccountWhenUserIsAuthenticated() throws Exception {
|
||||
loginPage.open();
|
||||
loginPage.clickRegister();
|
||||
String username1 = KeycloakModelUtils.generateId();
|
||||
registerPage.register("firstName", "lastName", username1 + "@mail.com", username1, "password", "password");
|
||||
verifyEmailPage.assertCurrent();
|
||||
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
|
||||
MimeMessage message = greenMail.getReceivedMessages()[0];
|
||||
String verificationLink1 = getEmailLink(message);
|
||||
|
||||
loginPage.open();
|
||||
loginPage.clickRegister();
|
||||
String username2 = KeycloakModelUtils.generateId();
|
||||
registerPage.register("firstName", "lastName", username2 + "@mail.com", username2, "password", "password");
|
||||
verifyEmailPage.assertCurrent();
|
||||
Assert.assertEquals(2, greenMail.getReceivedMessages().length);
|
||||
message = greenMail.getReceivedMessages()[1];
|
||||
String verificationLink2 = getEmailLink(message);
|
||||
driver.navigate().to(verificationLink2.trim());
|
||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
driver.navigate().to(verificationLink1.trim());
|
||||
assertTrue(errorPage.getError().contains("You are already authenticated as different user"));
|
||||
UserRepresentation user1 = testRealm().users().search(username1).get(0);
|
||||
UserRepresentation user2 = testRealm().users().search(username2).get(0);
|
||||
assertFalse(user1.isEmailVerified());
|
||||
assertTrue(user2.isEmailVerified());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyEmailFromAnotherAccountAfterEmalIsVerified() throws Exception {
|
||||
loginPage.open();
|
||||
loginPage.clickRegister();
|
||||
String username1 = KeycloakModelUtils.generateId();
|
||||
registerPage.register("firstName", "lastName", username1 + "@mail.com", username1, "password", "password");
|
||||
verifyEmailPage.assertCurrent();
|
||||
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
|
||||
MimeMessage message = greenMail.getReceivedMessages()[0];
|
||||
String verificationLink1 = getEmailLink(message);
|
||||
|
||||
loginPage.open();
|
||||
loginPage.clickRegister();
|
||||
String username2 = KeycloakModelUtils.generateId();
|
||||
registerPage.register("firstName", "lastName", username2 + "@mail.com", username2, "password", "password");
|
||||
verifyEmailPage.assertCurrent();
|
||||
Assert.assertEquals(2, greenMail.getReceivedMessages().length);
|
||||
message = greenMail.getReceivedMessages()[1];
|
||||
String verificationLink2 = getEmailLink(message);
|
||||
|
||||
driver.navigate().to(verificationLink1.trim());
|
||||
driver.navigate().to(verificationLink2.trim());
|
||||
assertTrue(errorPage.getError().contains("You are already authenticated as different user"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyEmailResend() throws IOException, MessagingException {
|
||||
loginPage.open();
|
||||
|
@ -250,7 +307,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
Assert.assertEquals(2, greenMail.getReceivedMessages().length);
|
||||
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
driver.navigate().to(verificationUrl.trim());
|
||||
|
||||
|
@ -294,7 +351,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
Assert.assertEquals(2, greenMail.getReceivedMessages().length);
|
||||
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
driver.navigate().to(verificationUrl.trim());
|
||||
|
||||
|
@ -324,7 +381,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
|
||||
MimeMessage message1 = greenMail.getReceivedMessages()[0];
|
||||
|
||||
String verificationUrl1 = getPasswordResetEmailLink(message1);
|
||||
String verificationUrl1 = getEmailLink(message1);
|
||||
|
||||
driver.navigate().to(verificationUrl1.trim());
|
||||
|
||||
|
@ -333,12 +390,16 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
|
||||
MimeMessage message2 = greenMail.getReceivedMessages()[1];
|
||||
|
||||
String verificationUrl2 = getPasswordResetEmailLink(message2);
|
||||
String verificationUrl2 = getEmailLink(message2);
|
||||
|
||||
events.clear();
|
||||
driver.navigate().to(verificationUrl2.trim());
|
||||
|
||||
events.expectRequiredAction(EventType.VERIFY_EMAIL)
|
||||
.error(Errors.EMAIL_ALREADY_VERIFIED)
|
||||
.detail(Details.REDIRECT_URI, Matchers.any(String.class))
|
||||
.assertEvent();
|
||||
infoPage.assertCurrent();
|
||||
Assert.assertEquals("You are already logged in.", infoPage.getInfo());
|
||||
Assert.assertEquals("Your email address has been verified already.", infoPage.getInfo());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -354,7 +415,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
|
||||
MimeMessage message1 = greenMail.getReceivedMessages()[0];
|
||||
|
||||
String verificationUrl1 = getPasswordResetEmailLink(message1);
|
||||
String verificationUrl1 = getEmailLink(message1);
|
||||
|
||||
driver.navigate().to(verificationUrl1.trim());
|
||||
|
||||
|
@ -362,14 +423,31 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
|
||||
MimeMessage message2 = greenMail.getReceivedMessages()[1];
|
||||
|
||||
String verificationUrl2 = getPasswordResetEmailLink(message2);
|
||||
String verificationUrl2 = getEmailLink(message2);
|
||||
|
||||
driver.navigate().to(verificationUrl2.trim());
|
||||
|
||||
proceedPage.assertCurrent();
|
||||
proceedPage.clickProceedLink();
|
||||
infoPage.assertCurrent();
|
||||
assertEquals("Your email address has been verified.", infoPage.getInfo());
|
||||
assertEquals("Your email address has been verified already.", infoPage.getInfo());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyEmailResendAndVerifyWithLatestLink() throws IOException, MessagingException {
|
||||
// Email verification can be performed any number of times
|
||||
loginPage.open();
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
verifyEmailPage.clickResendEmail();
|
||||
verifyEmailPage.assertCurrent();
|
||||
Assert.assertEquals(2, greenMail.getReceivedMessages().length);
|
||||
MimeMessage message1 = greenMail.getReceivedMessages()[0];
|
||||
String verificationUrl1 = getEmailLink(message1);
|
||||
|
||||
MimeMessage message2 = greenMail.getReceivedMessages()[1];
|
||||
String verificationUrl2 = getEmailLink(message2);
|
||||
driver.navigate().to(verificationUrl2.trim());
|
||||
appPage.assertCurrent();
|
||||
|
||||
driver.navigate().to(verificationUrl1.trim());
|
||||
assertEquals("Your email address has been verified already.", infoPage.getInfo());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -383,7 +461,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
AssertEvents.ExpectedEvent emailEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost");
|
||||
EventRepresentation sendEvent = emailEvent.assertEvent();
|
||||
|
@ -422,7 +500,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
verificationUrl = KeycloakUriBuilder.fromUri(verificationUrl).replaceQueryParam(Constants.KEY, "foo").build().toString();
|
||||
|
||||
|
@ -453,7 +531,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
events.poll();
|
||||
|
||||
|
@ -495,7 +573,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
events.poll();
|
||||
|
||||
|
@ -540,7 +618,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
events.poll();
|
||||
|
||||
|
@ -578,7 +656,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
events.poll();
|
||||
|
||||
|
@ -606,7 +684,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
}
|
||||
|
||||
|
||||
public static String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException {
|
||||
public static String getEmailLink(MimeMessage message) throws IOException, MessagingException {
|
||||
return MailUtils.getPasswordResetEmailLink(message);
|
||||
}
|
||||
|
||||
|
@ -621,7 +699,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
driver.manage().deleteAllCookies();
|
||||
|
||||
|
@ -650,7 +728,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
// open link in the second browser without the session
|
||||
driver2.navigate().to(verificationUrl.trim());
|
||||
|
@ -688,7 +766,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
driver.navigate().to(verificationUrl.trim());
|
||||
|
||||
|
@ -707,7 +785,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
driver.manage().deleteAllCookies();
|
||||
|
||||
|
@ -732,7 +810,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
driver.manage().deleteAllCookies();
|
||||
|
||||
|
@ -808,7 +886,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
driver2.navigate().to(verificationUrl.trim());
|
||||
|
||||
|
@ -846,7 +924,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
// confirm
|
||||
driver.navigate().to(verificationUrl);
|
||||
|
@ -856,7 +934,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
|
||||
// email should be verified and required actions empty
|
||||
UserRepresentation user = testRealm().users().get(testUserId).toRepresentation();
|
||||
Assert.assertTrue(user.isEmailVerified());
|
||||
assertTrue(user.isEmailVerified());
|
||||
assertThat(user.getRequiredActions(), Matchers.empty());
|
||||
}
|
||||
|
||||
|
@ -885,7 +963,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
// confirm
|
||||
driver.navigate().to(verificationUrl);
|
||||
|
@ -895,7 +973,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
|
||||
// email should be verified and required actions empty
|
||||
UserRepresentation user = testRealm().users().get(testUserId).toRepresentation();
|
||||
Assert.assertTrue(user.isEmailVerified());
|
||||
assertTrue(user.isEmailVerified());
|
||||
assertThat(user.getRequiredActions(), Matchers.empty());
|
||||
}
|
||||
|
||||
|
@ -917,7 +995,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
// confirm in the second browser
|
||||
driver2.navigate().to(verificationUrl);
|
||||
|
@ -939,7 +1017,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
|
||||
// email should be verified and required actions empty
|
||||
UserRepresentation user = testRealm().users().get(testUserId).toRepresentation();
|
||||
Assert.assertTrue(user.isEmailVerified());
|
||||
assertTrue(user.isEmailVerified());
|
||||
assertThat(user.getRequiredActions(), Matchers.empty());
|
||||
|
||||
// after refresh in the first browser the app should be shown
|
||||
|
@ -964,7 +1042,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
try {
|
||||
setTimeOffset(360);
|
||||
|
@ -989,7 +1067,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
assertEquals(1, greenMail.getReceivedMessages().length);
|
||||
|
||||
MimeMessage message = greenMail.getReceivedMessages()[0];
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
UserResource user = testRealm().users().get(testUserId);
|
||||
UserRepresentation userRep = user.toRepresentation();
|
||||
|
|
|
@ -909,13 +909,14 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa
|
|||
assertTrue(adminClient.realm(bc.consumerRealmName()).users().get(consumerUser.getId()).toRepresentation().isEmailVerified());
|
||||
|
||||
driver.navigate().to(url);
|
||||
waitForPage(driver, "you are already logged in.", false);
|
||||
waitForPage(driver, "your email address has been verified already.", false);
|
||||
AccountHelper.logout(adminClient.realm(bc.consumerRealmName()), "consumer");
|
||||
|
||||
driver.navigate().to(url);
|
||||
waitForPage(driver, "confirm linking the account testuser of identity provider " + bc.getIDPAlias() + " with your account.", false);
|
||||
proceedPage.clickProceedLink();
|
||||
waitForPage(driver, "you successfully verified your email. please go back to your original browser and continue there with the login.", false);
|
||||
waitForPage(driver, "your email address has been verified already.", false);
|
||||
|
||||
driver2.navigate().to(url);
|
||||
waitForPage(driver, "your email address has been verified already.", false);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -92,7 +92,7 @@ import static org.keycloak.storage.UserStorageProviderModel.EVICTION_HOUR;
|
|||
import static org.keycloak.storage.UserStorageProviderModel.EVICTION_MINUTE;
|
||||
import static org.keycloak.storage.UserStorageProviderModel.IMPORT_ENABLED;
|
||||
import static org.keycloak.storage.UserStorageProviderModel.MAX_LIFESPAN;
|
||||
import static org.keycloak.testsuite.actions.RequiredActionEmailVerificationTest.getPasswordResetEmailLink;
|
||||
import static org.keycloak.testsuite.actions.RequiredActionEmailVerificationTest.getEmailLink;
|
||||
|
||||
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith;
|
||||
|
||||
|
@ -393,7 +393,7 @@ public class UserStorageTest extends AbstractAuthTest {
|
|||
|
||||
MimeMessage message = greenMail.getReceivedMessages()[0];
|
||||
|
||||
String verificationUrl = getPasswordResetEmailLink(message);
|
||||
String verificationUrl = getEmailLink(message);
|
||||
|
||||
driver.navigate().to(verificationUrl.trim());
|
||||
|
||||
|
|
|
@ -353,6 +353,7 @@ realmSupportsNoCredentialsMessage=Realm does not support any credential type.
|
|||
credentialSetupRequired=Cannot login, credential setup required.
|
||||
identityProviderNotUniqueMessage=Realm supports multiple identity providers. Could not determine which identity provider should be used to authenticate with.
|
||||
emailVerifiedMessage=Your email address has been verified.
|
||||
emailVerifiedAlreadyMessage=Your email address has been verified already.
|
||||
staleEmailVerificationLink=The link you clicked is an old stale link and is no longer valid. Maybe you have already verified your email.
|
||||
identityProviderAlreadyLinkedMessage=Federated identity returned by {0} is already linked to another user.
|
||||
confirmAccountLinking=Confirm linking the account {0} of identity provider {1} with your account.
|
||||
|
|
Loading…
Reference in a new issue