From b5a854b68e1ad9979da470fd4a0a0126dbc4dd51 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Tue, 14 May 2024 08:19:02 -0300 Subject: [PATCH] Minor improvements to invitation email templates (#29498) Signed-off-by: Pedro Igor --- .../keycloak/email/EmailTemplateProvider.java | 3 +- .../FreeMarkerEmailTemplateProvider.java | 6 ++-- .../OrganizationInvitationResource.java | 2 +- .../resource/OrganizationMemberResource.java | 1 + .../admin/OrganizationInvitationLinkTest.java | 28 +++++++++++++------ .../theme/base/email/html/org-invite.ftl | 2 +- .../email/messages/messages_en.properties | 4 ++- .../theme/base/email/text/org-invite.ftl | 2 +- 8 files changed, 33 insertions(+), 15 deletions(-) diff --git a/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java b/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java index fc53d4f408..91160d15dc 100755 --- a/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java @@ -18,6 +18,7 @@ package org.keycloak.email; import org.keycloak.events.Event; +import org.keycloak.models.OrganizationModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.provider.Provider; @@ -77,7 +78,7 @@ public interface EmailTemplateProvider extends Provider { void sendVerifyEmail(String link, long expirationInMinutes) throws EmailException; - void sendOrgInviteEmail(String link, long expirationInMinutes) throws EmailException; + void sendOrgInviteEmail(OrganizationModel organization, String link, long expirationInMinutes) throws EmailException; void sendEmailUpdateConfirmation(String link, long expirationInMinutes, String address) throws EmailException; diff --git a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java index 57ed7a9e22..a5a93fb55c 100755 --- a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java +++ b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java @@ -37,6 +37,7 @@ import org.keycloak.events.EventType; import org.keycloak.forms.login.freemarker.model.UrlBean; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakUriInfo; +import org.keycloak.models.OrganizationModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.sessions.AuthenticationSessionModel; @@ -163,10 +164,11 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { } @Override - public void sendOrgInviteEmail(String link, long expirationInMinutes) throws EmailException { + public void sendOrgInviteEmail(OrganizationModel organization, String link, long expirationInMinutes) throws EmailException { Map attributes = new HashMap<>(this.attributes); addLinkInfoIntoAttributes(link, expirationInMinutes, attributes); - send("orgInviteSubject", "org-invite.ftl", attributes); + attributes.put("organization", organization); + send("orgInviteSubject", List.of(organization.getName()), "org-invite.ftl", attributes); } @Override diff --git a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationInvitationResource.java b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationInvitationResource.java index d0922fd2f7..c3069a4a23 100644 --- a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationInvitationResource.java +++ b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationInvitationResource.java @@ -110,7 +110,7 @@ public class OrganizationInvitationResource { session.getProvider(EmailTemplateProvider.class) .setRealm(realm) .setUser(user) - .sendOrgInviteEmail(link, TimeUnit.SECONDS.toMinutes(tokenExpiration)); + .sendOrgInviteEmail(organization, link, TimeUnit.SECONDS.toMinutes(tokenExpiration)); } catch (EmailException e) { ServicesLogger.LOGGER.failedToSendEmail(e); throw ErrorResponse.error("Failed to send invite email", Status.INTERNAL_SERVER_ERROR); diff --git a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationMemberResource.java b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationMemberResource.java index 02cfe3aecb..dbd2846f99 100644 --- a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationMemberResource.java +++ b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationMemberResource.java @@ -118,6 +118,7 @@ public class OrganizationMemberResource { } @Path("invite-user") + @POST public Response inviteUser(String email) { return new OrganizationInvitationResource(session, organization, adminEvent).inviteUser(email); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationInvitationLinkTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationInvitationLinkTest.java index de1ff3856f..6004fffbe8 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationInvitationLinkTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationInvitationLinkTest.java @@ -19,12 +19,14 @@ package org.keycloak.testsuite.organization.admin; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertTrue; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import jakarta.ws.rs.core.Response; import org.jboss.arquillian.graphene.page.Page; @@ -33,8 +35,6 @@ import org.junit.Test; import org.keycloak.admin.client.resource.OrganizationResource; import org.keycloak.common.Profile.Feature; import org.keycloak.common.util.UriUtils; -import org.keycloak.cookie.CookieProvider; -import org.keycloak.cookie.CookieScope; import org.keycloak.cookie.CookieType; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; @@ -46,6 +46,7 @@ import org.keycloak.testsuite.pages.InfoPage; import org.keycloak.testsuite.pages.RegisterPage; import org.keycloak.testsuite.util.GreenMailRule; import org.keycloak.testsuite.util.MailUtils; +import org.keycloak.testsuite.util.MailUtils.EmailBody; import org.keycloak.testsuite.util.UserBuilder; @EnableFeature(Feature.ORGANIZATION) @@ -71,7 +72,7 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest { } @Test - public void testInviteExistingUser() throws IOException { + public void testInviteExistingUser() throws IOException, MessagingException { UserRepresentation user = UserBuilder.create() .username("invited") .email("invited@myemail.com") @@ -88,7 +89,9 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest { MimeMessage message = greenMail.getLastReceivedMessage(); Assert.assertNotNull(message); - String link = MailUtils.getPasswordResetEmailLink(message); + Assert.assertEquals("Invitation to join the " + organizationName + " organization", message.getSubject()); + EmailBody body = MailUtils.getBody(message); + String link = MailUtils.getLink(body.getHtml()); driver.navigate().to(link.trim()); // not yet a member Assert.assertFalse(organization.members().getAll().stream().anyMatch(actual -> user.getId().equals(actual.getId()))); @@ -100,7 +103,7 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest { } @Test - public void testInviteNewUserRegistration() throws IOException { + public void testInviteNewUserRegistration() throws IOException, MessagingException { UserRepresentation user = UserBuilder.create() .username("invitedUser") .email("inviteduser@email") @@ -112,7 +115,14 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest { MimeMessage message = greenMail.getLastReceivedMessage(); Assert.assertNotNull(message); - String link = MailUtils.getPasswordResetEmailLink(message); + Assert.assertEquals("Invitation to join the " + organizationName + " organization", message.getSubject()); + EmailBody body = MailUtils.getBody(message); + String link = MailUtils.getLink(body.getHtml()); + String text = body.getHtml(); + assertTrue(text.contains("

You were invited to join the " + organizationName + " organization. Click the link below to join.

")); + assertTrue(text.contains("Link to join the organization

")); + assertTrue(text.contains("Link to join the organization")); + assertTrue(text.contains("

If you dont want to join the organization, just ignore this message.

")); String orgToken = UriUtils.parseQueryParameters(link, false).values().stream().map(strings -> strings.get(0)).findFirst().orElse(null); Assert.assertNotNull(orgToken); driver.navigate().to(link.trim()); @@ -144,7 +154,8 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest { MimeMessage message = greenMail.getLastReceivedMessage(); Assert.assertNotNull(message); - String link = MailUtils.getPasswordResetEmailLink(message); + EmailBody body = MailUtils.getBody(message); + String link = MailUtils.getLink(body.getHtml()); String orgToken = UriUtils.parseQueryParameters(link, false).values().stream().map(strings -> strings.get(0)).findFirst().orElse(null); Assert.assertNotNull(orgToken); driver.navigate().to(link.trim()); @@ -173,7 +184,8 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest { setTimeOffset((int) TimeUnit.DAYS.toSeconds(1)); MimeMessage message = greenMail.getLastReceivedMessage(); Assert.assertNotNull(message); - String link = MailUtils.getPasswordResetEmailLink(message); + EmailBody body = MailUtils.getBody(message); + String link = MailUtils.getLink(body.getHtml()); String orgToken = UriUtils.parseQueryParameters(link, false).values().stream().map(strings -> strings.get(0)).findFirst().orElse(null); Assert.assertNotNull(orgToken); driver.navigate().to(link.trim()); diff --git a/themes/src/main/resources/theme/base/email/html/org-invite.ftl b/themes/src/main/resources/theme/base/email/html/org-invite.ftl index d7f72e3e20..78811f0eda 100644 --- a/themes/src/main/resources/theme/base/email/html/org-invite.ftl +++ b/themes/src/main/resources/theme/base/email/html/org-invite.ftl @@ -1,4 +1,4 @@ <#import "template.ftl" as layout> <@layout.emailLayout> -${kcSanitize(msg("orgInviteBodyHtml", link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration)))?no_esc} +${kcSanitize(msg("orgInviteBodyHtml", link, linkExpiration, realmName, organization.name, linkExpirationFormatter(linkExpiration)))?no_esc} diff --git a/themes/src/main/resources/theme/base/email/messages/messages_en.properties b/themes/src/main/resources/theme/base/email/messages/messages_en.properties index dc593deada..b40fe28de4 100755 --- a/themes/src/main/resources/theme/base/email/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/email/messages/messages_en.properties @@ -1,7 +1,9 @@ emailVerificationSubject=Verify email emailVerificationBody=Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address\n\n{0}\n\nThis link will expire within {3}.\n\nIf you didn''t create this account, just ignore this message. emailVerificationBodyHtml=

Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address

Link to e-mail address verification

This link will expire within {3}.

If you didn''t create this account, just ignore this message.

-orgInviteBodyHtml=

Someone has invited your account {2} account to join their keycloak organization! Click the link below to join.

Link to join the organization

This link will expire within {3}.

If you don't want to join the organization, just ignore this message.

+orgInviteSubject=Invitation to join the {0} organization +orgInviteBody=You were invited to join the "{3}" organization. Click the link below to join.\n\n{0}\n\nThis link will expire within {4}.\n\nIf you don't want to join the organization, just ignore this message. +orgInviteBodyHtml=

You were invited to join the {3} organization. Click the link below to join.

Link to join the organization

This link will expire within {4}.

If you don't want to join the organization, just ignore this message.

emailUpdateConfirmationSubject=Verify new email emailUpdateConfirmationBody=To update your {2} account with email address {1}, click the link below\n\n{0}\n\nThis link will expire within {3}.\n\nIf you don''t want to proceed with this modification, just ignore this message. emailUpdateConfirmationBodyHtml=

To update your {2} account with email address {1}, click the link below

{0}

This link will expire within {3}.

If you don''t want to proceed with this modification, just ignore this message.

diff --git a/themes/src/main/resources/theme/base/email/text/org-invite.ftl b/themes/src/main/resources/theme/base/email/text/org-invite.ftl index afc32a7230..b74abe0d0a 100644 --- a/themes/src/main/resources/theme/base/email/text/org-invite.ftl +++ b/themes/src/main/resources/theme/base/email/text/org-invite.ftl @@ -1,2 +1,2 @@ <#ftl output_format="plainText"> -${kcSanitize(msg("orgInviteBodyHtml", link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration)))} +${kcSanitize(msg("orgInviteBody", link, linkExpiration, realmName, organization.name, linkExpirationFormatter(linkExpiration)))}