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}
@layout.emailLayout>
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)))}