Fix: Remove VERIFY_EMAIL action and publish event in ExecuteActionsActionTokenHandler

When processing execute-actions token, the email was marked as verified
but the VERIFY_EMAIL required action was not removed and no event was
published. This led to inconsistent state.

Now properly:
- Removes VERIFY_EMAIL from user and auth session
- Publishes VERIFY_EMAIL event for audit trail
- Only performs these actions if email was not already verified

Closes #42875

Signed-off-by: Rathan Naik <30756840+Rathan-Naik@users.noreply.github.com>
This commit is contained in:
Rathan Naik 2026-01-15 19:56:22 +05:30 committed by Rathan-Naik
parent 6ceaa2d391
commit 005cef4fcb
2 changed files with 46 additions and 1 deletions

View file

@ -31,6 +31,7 @@ import org.keycloak.authentication.actiontoken.AbstractActionTokenHandler;
import org.keycloak.authentication.actiontoken.ActionTokenContext;
import org.keycloak.authentication.actiontoken.TokenUtils;
import org.keycloak.authentication.requiredactions.util.RequiredActionsValidator;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.forms.login.LoginFormsProvider;
@ -119,7 +120,15 @@ public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHandler
UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser();
// verify user email as we know it is valid as this entry point would never have gotten here.
user.setEmailVerified(true);
if (!user.isEmailVerified()) {
user.setEmailVerified(true);
user.removeRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
authSession.removeRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
tokenContext.getEvent().clone()
.event(EventType.VERIFY_EMAIL)
.detail(Details.EMAIL, user.getEmail())
.success();
}
String nextAction = AuthenticationManager.nextRequiredAction(tokenContext.getSession(), authSession, tokenContext.getRequest(), tokenContext.getEvent());
return AuthenticationManager.redirectToRequiredActions(tokenContext.getSession(), tokenContext.getRealm(), authSession, tokenContext.getUriInfo(), nextAction);

View file

@ -1216,4 +1216,40 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
assertTrue(userRep.isEmailVerified());
assertThat(userRep.getRequiredActions(), Matchers.empty());
}
@Test
public void executeActionsEmailVerifiesEmailAndRemovesRequiredAction() throws IOException {
try (Closeable u = new UserAttributeUpdater(testRealm().users().get(testUserId))
.setEmailVerified(false)
.setRequiredActions(RequiredAction.VERIFY_EMAIL)
.update()) {
UserRepresentation userBefore = testRealm().users().get(testUserId).toRepresentation();
assertFalse(userBefore.isEmailVerified());
assertThat(userBefore.getRequiredActions(), Matchers.contains(RequiredAction.VERIFY_EMAIL.name()));
testRealm().users().get(testUserId).executeActionsEmail(List.of(RequiredAction.UPDATE_PASSWORD.name()));
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getLastReceivedMessage();
String verificationUrl = getEmailLink(message);
driver.manage().deleteAllCookies();
driver.navigate().to(verificationUrl.trim());
proceedPage.assertCurrent();
proceedPage.clickProceedLink();
events.expectRequiredAction(EventType.VERIFY_EMAIL)
.user(testUserId)
.client("account")
.detail(Details.EMAIL, "test-user@localhost")
.detail(Details.REDIRECT_URI, Matchers.any(String.class))
.assertEvent();
UserRepresentation userAfter = testRealm().users().get(testUserId).toRepresentation();
assertTrue(userAfter.isEmailVerified());
assertThat(userAfter.getRequiredActions(), Matchers.not(Matchers.contains(RequiredAction.VERIFY_EMAIL.name())));
}
}
}