Use note to detect the IDP verify email action is already done

Closes #31563

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc 2024-09-26 13:49:46 +02:00 committed by Marek Posolda
parent 90dc7c168c
commit 1d23c3c720
3 changed files with 74 additions and 13 deletions

View file

@ -30,7 +30,6 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionCompoundId; import org.keycloak.sessions.AuthenticationSessionCompoundId;
@ -81,17 +80,21 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession(); AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
if (user.isEmailVerified() && !isVerifyEmailActionSet(user, authSession)) { if (authSession.getAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME) != null) {
event.user(user).error(Errors.EMAIL_ALREADY_VERIFIED); return sendEmailAlreadyVerified(session, event, user);
return session.getProvider(LoginFormsProvider.class)
.setAuthenticationSession(session.getContext().getAuthenticationSession())
.setInfo(Messages.EMAIL_VERIFIED_ALREADY, user.getEmail())
.createInfoPage();
} }
event.success(); AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
if (tokenContext.isAuthenticationSessionFresh()) { if (tokenContext.isAuthenticationSessionFresh()) {
AuthenticationSessionCompoundId compoundId = AuthenticationSessionCompoundId.encoded(token.getCompoundAuthenticationSessionId());
ClientModel originalClient = realm.getClientById(compoundId.getClientUUID());
AuthenticationSessionModel origAuthSession = asm.getAuthenticationSessionByIdAndClient(realm,
compoundId.getRootSessionId(), originalClient, compoundId.getTabId());
if (origAuthSession == null || origAuthSession.getAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME) != null) {
return sendEmailAlreadyVerified(session, event, user);
}
token.setOriginalCompoundAuthenticationSessionId(token.getCompoundAuthenticationSessionId()); token.setOriginalCompoundAuthenticationSessionId(token.getCompoundAuthenticationSessionId());
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId(); String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
@ -109,9 +112,9 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
// verify user email as we know it is valid as this entry point would never have gotten here. // verify user email as we know it is valid as this entry point would never have gotten here.
user.setEmailVerified(true); user.setEmailVerified(true);
event.success();
if (token.getOriginalCompoundAuthenticationSessionId() != null) { if (token.getOriginalCompoundAuthenticationSessionId() != null) {
AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
asm.removeAuthenticationSession(realm, authSession, true); asm.removeAuthenticationSession(realm, authSession, true);
AuthenticationSessionCompoundId compoundId = AuthenticationSessionCompoundId.encoded(token.getOriginalCompoundAuthenticationSessionId()); AuthenticationSessionCompoundId compoundId = AuthenticationSessionCompoundId.encoded(token.getOriginalCompoundAuthenticationSessionId());
@ -140,8 +143,11 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
return tokenContext.brokerFlow(null, null, authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH)); return tokenContext.brokerFlow(null, null, authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH));
} }
private boolean isVerifyEmailActionSet(UserModel user, AuthenticationSessionModel authSession) { private Response sendEmailAlreadyVerified(KeycloakSession session, EventBuilder event, UserModel user) {
return Stream.concat(user.getRequiredActionsStream(), authSession.getRequiredActions().stream()) event.user(user).error(Errors.EMAIL_ALREADY_VERIFIED);
.anyMatch(RequiredAction.VERIFY_EMAIL.name()::equals); return session.getProvider(LoginFormsProvider.class)
.setAuthenticationSession(session.getContext().getAuthenticationSession())
.setInfo(Messages.EMAIL_VERIFIED_ALREADY, user.getEmail())
.createInfoPage();
} }
} }

View file

@ -28,9 +28,12 @@ public class IdpLinkEmailPage extends AbstractPage {
@FindBy(id = "instruction1") @FindBy(id = "instruction1")
private WebElement message; private WebElement message;
@FindBy(linkText = "Click here") @FindBy(xpath = "//p[@id='instruction2']/a[text() = 'Click here']")
private WebElement resendEmailLink; private WebElement resendEmailLink;
@FindBy(xpath = "//p[@id='instruction3']/a[text() = 'Click here']")
private WebElement continueLink;
@Override @Override
public boolean isCurrent() { public boolean isCurrent() {
return PageUtils.getPageTitle(driver).startsWith("Link "); return PageUtils.getPageTitle(driver).startsWith("Link ");
@ -48,4 +51,8 @@ public class IdpLinkEmailPage extends AbstractPage {
public void resendEmail() { public void resendEmail() {
resendEmailLink.click(); resendEmailLink.click();
} }
public void continueLink() {
continueLink.click();
}
} }

View file

@ -47,6 +47,7 @@ import org.openqa.selenium.support.PageFactory;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.keycloak.storage.UserStorageProviderModel.IMPORT_ENABLED; import static org.keycloak.storage.UserStorageProviderModel.IMPORT_ENABLED;
import static org.keycloak.testsuite.admin.ApiUtil.removeUserByUsername; import static org.keycloak.testsuite.admin.ApiUtil.removeUserByUsername;
@ -1018,6 +1019,53 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa
waitForPage(driver, "your email address has been verified already.", false); waitForPage(driver, "your email address has been verified already.", false);
} }
@Test
public void testLinkAccountByEmailVerificationInAnotherBrowser() {
RealmResource realm = adminClient.realm(bc.consumerRealmName());
UserResource userResource = realm.users().get(createUser("consumer"));
UserRepresentation consumerUser = userResource.toRepresentation();
consumerUser.setEmail(bc.getUserEmail());
consumerUser.setEmailVerified(true);
userResource.update(consumerUser);
configureSMTPServer();
oauth.clientId("broker-app");
loginPage.open(bc.consumerRealmName());
logInWithBroker(bc);
//link account by email
waitForPage(driver, "update account information", false);
Assert.assertTrue(updateAccountInformationPage.isCurrent());
updateAccountInformationPage.updateAccountInformation("FirstName", "LastName");
waitForPage(driver, "account already exists", false);
assertTrue(idpConfirmLinkPage.isCurrent());
assertEquals("User with email user@localhost.com already exists. How do you want to continue?", idpConfirmLinkPage.getMessage());
idpConfirmLinkPage.clickLinkAccount();
idpLinkEmailPage.assertCurrent();
String url = assertEmailAndGetUrl(MailServerConfiguration.FROM, USER_EMAIL,
"Someone wants to link your ", false);
// in the second browser confirm the mail
driver2.navigate().to(url);
assertThat(driver2.findElement(By.className("instruction")).getText(), startsWith("Confirm linking the account"));
driver2.findElement(By.linkText("» Click here to proceed")).click();
assertThat(driver2.findElement(By.className("instruction")).getText(), startsWith("You successfully verified your email."));
idpLinkEmailPage.continueLink();
//test if user is logged in
assertTrue(driver.getCurrentUrl().startsWith(getConsumerRoot() + "/auth/realms/master/app/"));
// check user is linked
List<FederatedIdentityRepresentation> identities = userResource.getFederatedIdentity();
assertEquals(1, identities.size());
assertEquals(bc.getIDPAlias(), identities.iterator().next().getIdentityProvider());
assertEquals(bc.getUserLogin(), identities.iterator().next().getUserName());
}
@Test @Test
public void testLinkAccountByEmailVerificationToEmailVerifiedUser() { public void testLinkAccountByEmailVerificationToEmailVerifiedUser() {
// set up a user with verified email // set up a user with verified email