diff --git a/services/src/main/java/org/keycloak/email/freemarker/beans/EventBean.java b/services/src/main/java/org/keycloak/email/freemarker/beans/EventBean.java index d255c9c0cf..4edb6e2420 100644 --- a/services/src/main/java/org/keycloak/email/freemarker/beans/EventBean.java +++ b/services/src/main/java/org/keycloak/email/freemarker/beans/EventBean.java @@ -58,6 +58,12 @@ public class EventBean { return details; } + public String getDetail(String name) { + return event.getDetails() != null + ? event.getDetails().get(name) + : null; + } + public static class DetailBean { private Map.Entry entry; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionDeleteCredentialTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionDeleteCredentialTest.java index 0c96b5eafb..2b76f9d9b5 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionDeleteCredentialTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionDeleteCredentialTest.java @@ -19,17 +19,21 @@ package org.keycloak.testsuite.actions; -import java.util.List; - +import jakarta.mail.internet.MimeMessage; import jakarta.ws.rs.core.Response; +import java.util.List; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; import org.jboss.arquillian.graphene.page.Page; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.keycloak.authentication.requiredactions.DeleteCredentialAction; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventType; +import org.keycloak.events.email.EmailEventListenerProviderFactory; import org.keycloak.models.credential.OTPCredentialModel; import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.models.utils.TimeBasedOTP; @@ -41,6 +45,9 @@ import org.keycloak.testsuite.pages.DeleteCredentialPage; import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.LoginConfigTotpPage; import org.keycloak.testsuite.pages.LoginTotpPage; +import org.keycloak.testsuite.updaters.RealmAttributeUpdater; +import org.keycloak.testsuite.util.GreenMailRule; +import org.keycloak.testsuite.util.MailUtils; import org.keycloak.testsuite.util.UserBuilder; /** @@ -48,6 +55,9 @@ import org.keycloak.testsuite.util.UserBuilder; */ public class AppInitiatedActionDeleteCredentialTest extends AbstractAppInitiatedActionTest { + @Rule + public GreenMailRule greenMail = new GreenMailRule(); + @Override protected String getAiaAction() { return DeleteCredentialAction.PROVIDER_ID; @@ -76,9 +86,11 @@ public class AppInitiatedActionDeleteCredentialTest extends AbstractAppInitiated @Before public void beforeTest() { + ApiUtil.removeUserByUsername(testRealm(), "test-user@localhost"); UserRepresentation user = UserBuilder.create() .username("john") - .email("john@email.cz") + .email("test-user@localhost") + .emailVerified(true) .firstName("John") .lastName("Bar") .enabled(true) @@ -92,33 +104,48 @@ public class AppInitiatedActionDeleteCredentialTest extends AbstractAppInitiated @Test public void removeOtpSuccess() throws Exception { - String credentialId = getCredentialIdByType(OTPCredentialModel.TYPE); - oauth.kcAction(getKcActionParamForDeleteCredential(credentialId)); + try (RealmAttributeUpdater updater = new RealmAttributeUpdater(testRealm()) + .addEventsListener(EmailEventListenerProviderFactory.ID) + .update()) { - loginPasswordAndOtp(); + String credentialId = getCredentialIdByType(OTPCredentialModel.TYPE); + oauth.kcAction(getKcActionParamForDeleteCredential(credentialId)); - deleteCredentialPage.assertCurrent(); - deleteCredentialPage.assertCredentialInMessage(OTPCredentialModel.TYPE); + loginPasswordAndOtp(); - deleteCredentialPage.confirm(); + deleteCredentialPage.assertCurrent(); + deleteCredentialPage.assertCredentialInMessage(OTPCredentialModel.TYPE); - appPage.assertCurrent(); - assertKcActionStatus("success"); + deleteCredentialPage.confirm(); - Assert.assertNull(getCredentialIdByType(OTPCredentialModel.TYPE)); + appPage.assertCurrent(); + assertKcActionStatus("success"); - events.expect(EventType.REMOVE_TOTP) - .user(userId) - .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) - .detail(Details.CREDENTIAL_ID, credentialId) - .detail(Details.CUSTOM_REQUIRED_ACTION, DeleteCredentialAction.PROVIDER_ID) - .assertEvent(); - events.expect(EventType.REMOVE_CREDENTIAL) - .user(userId) - .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) - .detail(Details.CREDENTIAL_ID, credentialId) - .detail(Details.CUSTOM_REQUIRED_ACTION, DeleteCredentialAction.PROVIDER_ID) - .assertEvent(); + Assert.assertNull(getCredentialIdByType(OTPCredentialModel.TYPE)); + + events.expect(EventType.REMOVE_TOTP) + .user(userId) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .detail(Details.CREDENTIAL_ID, credentialId) + .detail(Details.CUSTOM_REQUIRED_ACTION, DeleteCredentialAction.PROVIDER_ID) + .assertEvent(); + events.expect(EventType.REMOVE_CREDENTIAL) + .user(userId) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .detail(Details.CREDENTIAL_ID, credentialId) + .detail(Details.CUSTOM_REQUIRED_ACTION, DeleteCredentialAction.PROVIDER_ID) + .assertEvent(); + + MimeMessage[] receivedMessages = greenMail.getReceivedMessages(); + Assert.assertEquals(2, receivedMessages.length); + + Assert.assertEquals("Remove OTP", receivedMessages[0].getSubject()); + Assert.assertEquals("Remove credential", receivedMessages[1].getSubject()); + MatcherAssert.assertThat(MailUtils.getBody(receivedMessages[1]).getText(), + Matchers.startsWith("Credential otp was removed from your account")); + MatcherAssert.assertThat(MailUtils.getBody(receivedMessages[1]).getHtml(), + Matchers.containsString("Credential otp was removed from your account")); + } } @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionResetPasswordTest.java index 2fdcf54878..32b16192f1 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionResetPasswordTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionResetPasswordTest.java @@ -16,6 +16,9 @@ */ package org.keycloak.testsuite.actions; +import jakarta.mail.internet.MimeMessage; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; import org.jboss.arquillian.drone.api.annotation.Drone; import org.jboss.arquillian.graphene.page.Page; import org.junit.After; @@ -26,6 +29,7 @@ import org.keycloak.admin.client.resource.UserResource; import org.keycloak.cookie.CookieType; import org.keycloak.events.Details; import org.keycloak.events.EventType; +import org.keycloak.events.email.EmailEventListenerProviderFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.credential.PasswordCredentialModel; @@ -35,7 +39,10 @@ import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; +import org.keycloak.testsuite.updaters.RealmAttributeUpdater; +import org.keycloak.testsuite.updaters.UserAttributeUpdater; import org.keycloak.testsuite.util.GreenMailRule; +import org.keycloak.testsuite.util.MailUtils; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.SecondBrowser; import org.openqa.selenium.Cookie; @@ -81,48 +88,66 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct @Test public void resetPassword() throws Exception { - loginPage.open(); - loginPage.login("test-user@localhost", "password"); + try (RealmAttributeUpdater realmUpdater = new RealmAttributeUpdater(testRealm()) + .addEventsListener(EmailEventListenerProviderFactory.ID) + .update(); + UserAttributeUpdater userUpdater = new UserAttributeUpdater(ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost")) + .setEmailVerified(true) + .update()) { - events.expectLogin().assertEvent(); + loginPage.open(); + loginPage.login("test-user@localhost", "password"); - doAIA(); + events.expectLogin().assertEvent(); - changePasswordPage.assertCurrent(); - assertTrue(changePasswordPage.isCancelDisplayed()); + doAIA(); - Cookie authSessionCookie = driver.manage().getCookieNamed(CookieType.AUTH_SESSION_ID.getName()); - String authSessionId = authSessionCookie.getValue().split("\\.")[0]; - testingClient.server().run(session -> { - // ensure that our logic to detect the authentication session works as expected - RealmModel realm = session.realms().getRealm(TEST_REALM_NAME); - assertNotNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId)); - }); + changePasswordPage.assertCurrent(); + assertTrue(changePasswordPage.isCancelDisplayed()); - changePasswordPage.changePassword("new-password", "new-password"); + Cookie authSessionCookie = driver.manage().getCookieNamed(CookieType.AUTH_SESSION_ID.getName()); + String authSessionId = authSessionCookie.getValue().split("\\.")[0]; + testingClient.server().run(session -> { + // ensure that our logic to detect the authentication session works as expected + RealmModel realm = session.realms().getRealm(TEST_REALM_NAME); + assertNotNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId)); + }); - testingClient.server().run(session -> { - // ensure that the authentication session has been terminated - RealmModel realm = session.realms().getRealm(TEST_REALM_NAME); - assertNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId)); - }); + changePasswordPage.changePassword("new-password", "new-password"); - events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); - events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); + testingClient.server().run(session -> { + // ensure that the authentication session has been terminated + RealmModel realm = session.realms().getRealm(TEST_REALM_NAME); + assertNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId)); + }); - assertKcActionStatus(SUCCESS); + events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); - EventRepresentation loginEvent = events.expectLogin().assertEvent(); + MimeMessage[] receivedMessages = greenMail.getReceivedMessages(); + Assert.assertEquals(2, receivedMessages.length); - OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent); - oauth.idTokenHint(tokenResponse.getIdToken()).openLogout(); + Assert.assertEquals("Update password", receivedMessages[0].getSubject()); + Assert.assertEquals("Update credential", receivedMessages[1].getSubject()); + MatcherAssert.assertThat(MailUtils.getBody(receivedMessages[1]).getText(), + Matchers.startsWith("Your password credential was changed")); + MatcherAssert.assertThat(MailUtils.getBody(receivedMessages[1]).getHtml(), + Matchers.containsString("Your password credential was changed")); - events.expectLogout(loginEvent.getSessionId()).assertEvent(); + assertKcActionStatus(SUCCESS); - loginPage.open(); - loginPage.login("test-user@localhost", "new-password"); + EventRepresentation loginEvent = events.expectLogin().assertEvent(); - events.expectLogin().assertEvent(); + OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent); + oauth.idTokenHint(tokenResponse.getIdToken()).openLogout(); + + events.expectLogout(loginEvent.getSessionId()).assertEvent(); + + loginPage.open(); + loginPage.login("test-user@localhost", "new-password"); + + events.expectLogin().assertEvent(); + } } @Test diff --git a/themes/src/main/resources/theme/base/email/html/event-remove_credential.ftl b/themes/src/main/resources/theme/base/email/html/event-remove_credential.ftl index 28e5650a48..33b6dcfda2 100644 --- a/themes/src/main/resources/theme/base/email/html/event-remove_credential.ftl +++ b/themes/src/main/resources/theme/base/email/html/event-remove_credential.ftl @@ -1,4 +1,4 @@ <#import "template.ftl" as layout> <@layout.emailLayout> -${kcSanitize(msg("eventRemoveCredentialBodyHtml", event.details.credential_type!"unknown", event.date, event.ipAddress))?no_esc} +${kcSanitize(msg("eventRemoveCredentialBodyHtml", event.getDetail("credential_type")!"unknown", event.date, event.ipAddress))?no_esc} diff --git a/themes/src/main/resources/theme/base/email/html/event-update_credential.ftl b/themes/src/main/resources/theme/base/email/html/event-update_credential.ftl index 61aec8d658..f79e72d00a 100644 --- a/themes/src/main/resources/theme/base/email/html/event-update_credential.ftl +++ b/themes/src/main/resources/theme/base/email/html/event-update_credential.ftl @@ -1,4 +1,4 @@ <#import "template.ftl" as layout> <@layout.emailLayout> -${kcSanitize(msg("eventUpdateCredentialBodyHtml", event.details.credential_type!"unknown", event.date, event.ipAddress))?no_esc} +${kcSanitize(msg("eventUpdateCredentialBodyHtml", event.getDetail("credential_type")!"unknown", event.date, event.ipAddress))?no_esc} diff --git a/themes/src/main/resources/theme/base/email/text/event-remove_credential.ftl b/themes/src/main/resources/theme/base/email/text/event-remove_credential.ftl index 7e4c26d127..3143597d91 100644 --- a/themes/src/main/resources/theme/base/email/text/event-remove_credential.ftl +++ b/themes/src/main/resources/theme/base/email/text/event-remove_credential.ftl @@ -1,2 +1,2 @@ <#ftl output_format="plainText"> -${msg("eventRemoveCredentialBody", event.details.credential_type!"unknown", event.date, event.ipAddress)} \ No newline at end of file +${msg("eventRemoveCredentialBody", event.getDetail("credential_type")!"unknown", event.date, event.ipAddress)} diff --git a/themes/src/main/resources/theme/base/email/text/event-update_credential.ftl b/themes/src/main/resources/theme/base/email/text/event-update_credential.ftl index 95f0d05296..004813fc38 100644 --- a/themes/src/main/resources/theme/base/email/text/event-update_credential.ftl +++ b/themes/src/main/resources/theme/base/email/text/event-update_credential.ftl @@ -1,2 +1,2 @@ <#ftl output_format="plainText"> -${msg("eventUpdateCredentialBody", event.details.credential_type!"unknown", event.date, event.ipAddress)} \ No newline at end of file +${msg("eventUpdateCredentialBody", event.getDetail("credential_type")!"unknown", event.date, event.ipAddress)}