diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs
index 8ce27ea198..99c1ae93ef 100644
--- a/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs
+++ b/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs
@@ -2,7 +2,7 @@
- You have been invited to join the {{OrganizationName}} organization.
+ You have been invited to join the {{OrganizationName}} organization. This link expires on {{ExpirationDate}}.
|
diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.text.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.text.hbs
index 0f806836d1..ed0ec4261f 100644
--- a/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.text.hbs
+++ b/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.text.hbs
@@ -3,5 +3,7 @@ You have been invited to join the {{OrganizationName}} organization. To accept t
{{{Url}}}
+This link expires on {{ExpirationDate}}.
+
If you do not wish to join this organization, you can safely ignore this email.
-{{/BasicTextLayout}}
\ No newline at end of file
+{{/BasicTextLayout}}
diff --git a/src/Core/Models/Business/ExpiringToken.cs b/src/Core/Models/Business/ExpiringToken.cs
new file mode 100644
index 0000000000..c2a782de5b
--- /dev/null
+++ b/src/Core/Models/Business/ExpiringToken.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace Bit.Core.Models.Business
+{
+ public class ExpiringToken
+ {
+ public readonly string Token;
+ public readonly DateTime ExpirationDate;
+
+ public ExpiringToken(string token, DateTime expirationDate)
+ {
+ Token = token;
+ ExpirationDate = expirationDate;
+ }
+ }
+}
diff --git a/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs b/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs
index 653dccef6f..36509f5bbf 100644
--- a/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs
+++ b/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs
@@ -1,4 +1,6 @@
-namespace Bit.Core.Models.Mail
+using System;
+
+namespace Bit.Core.Models.Mail
{
public class OrganizationUserInvitedViewModel : BaseMailModel
{
@@ -8,6 +10,7 @@
public string Email { get; set; }
public string OrganizationNameUrlEncoded { get; set; }
public string Token { get; set; }
+ public string ExpirationDate { get; set; }
public string Url => string.Format("{0}/accept-organization?organizationId={1}&" +
"organizationUserId={2}&email={3}&organizationName={4}&token={5}",
WebVaultUrl,
diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs
index 43b4426f65..94d3478ba2 100644
--- a/src/Core/Services/IMailService.cs
+++ b/src/Core/Services/IMailService.cs
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System;
using Bit.Core.Models.Mail;
using Bit.Core.Models.Table.Provider;
+using Bit.Core.Models.Business;
namespace Bit.Core.Services
{
@@ -17,10 +18,9 @@ namespace Bit.Core.Services
Task SendTwoFactorEmailAsync(string email, string token);
Task SendNoMasterPasswordHintEmailAsync(string email);
Task SendMasterPasswordHintEmailAsync(string email, string hint);
- Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, string token);
- Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, string token)> invites);
- Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier,
- IEnumerable adminEmails);
+ Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token);
+ Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites);
+ Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable adminEmails);
Task SendOrganizationConfirmedEmailAsync(string organizationName, string email);
Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email);
Task SendPasswordlessSignInAsync(string returnUrl, string token, string email);
diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs
index f98a49d0d0..8aabb3a715 100644
--- a/src/Core/Services/Implementations/HandlebarsMailService.cs
+++ b/src/Core/Services/Implementations/HandlebarsMailService.cs
@@ -12,6 +12,7 @@ using System.Reflection;
using Bit.Core.Models.Mail.Provider;
using Bit.Core.Models.Table.Provider;
using HandlebarsDotNet;
+using Bit.Core.Models.Business;
namespace Bit.Core.Services
{
@@ -174,10 +175,10 @@ namespace Bit.Core.Services
await _mailDeliveryService.SendEmailAsync(message);
}
- public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, string token) =>
+ public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token) =>
BulkSendOrganizationInviteEmailAsync(organizationName, new[] { (orgUser, token) });
- public async Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, string token)> invites)
+ public async Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites)
{
MailQueueMessage CreateMessage(string email, object model)
{
@@ -192,7 +193,8 @@ namespace Bit.Core.Services
Email = WebUtility.UrlEncode(invite.orgUser.Email),
OrganizationId = invite.orgUser.OrganizationId.ToString(),
OrganizationUserId = invite.orgUser.Id.ToString(),
- Token = WebUtility.UrlEncode(invite.token),
+ Token = WebUtility.UrlEncode(invite.token.Token),
+ ExpirationDate = $"{invite.token.ExpirationDate.ToLongDateString()} {invite.token.ExpirationDate.ToShortTimeString()} UTC",
OrganizationNameUrlEncoded = WebUtility.UrlEncode(organizationName),
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs
index 276c4c49c1..ea2e758937 100644
--- a/src/Core/Services/Implementations/OrganizationService.cs
+++ b/src/Core/Services/Implementations/OrganizationService.cs
@@ -1289,15 +1289,16 @@ namespace Bit.Core.Services
string MakeToken(OrganizationUser orgUser) =>
_dataProtector.Protect($"OrganizationUserInvite {orgUser.Id} {orgUser.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
await _mailService.BulkSendOrganizationInviteEmailAsync(organization.Name,
- orgUsers.Select(o => (o, MakeToken(o))));
+ orgUsers.Select(o => (o, new ExpiringToken(MakeToken(o), DateTime.UtcNow.AddDays(5)))));
}
private async Task SendInviteAsync(OrganizationUser orgUser, Organization organization)
{
- var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
+ var now = DateTime.UtcNow;
+ var nowMillis = CoreHelpers.ToEpocMilliseconds(now);
var token = _dataProtector.Protect(
$"OrganizationUserInvite {orgUser.Id} {orgUser.Email} {nowMillis}");
- await _mailService.SendOrganizationInviteEmailAsync(organization.Name, orgUser, token);
+ await _mailService.SendOrganizationInviteEmailAsync(organization.Name, orgUser, new ExpiringToken(token, now.AddDays(5)));
}
public async Task AcceptUserAsync(Guid organizationUserId, User user, string token,
diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs
index 37ab39cb3d..5ca5997d19 100644
--- a/src/Core/Services/NoopImplementations/NoopMailService.cs
+++ b/src/Core/Services/NoopImplementations/NoopMailService.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
+using Bit.Core.Models.Business;
using Bit.Core.Models.Mail;
using Bit.Core.Models.Table;
using Bit.Core.Models.Table.Provider;
@@ -44,12 +45,12 @@ namespace Bit.Core.Services
return Task.FromResult(0);
}
- public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, string token)
+ public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token)
{
return Task.FromResult(0);
}
- public Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, string token)> invites)
+ public Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites)
{
return Task.FromResult(0);
}
diff --git a/test/Core.Test/Services/OrganizationServiceTests.cs b/test/Core.Test/Services/OrganizationServiceTests.cs
index 033202202b..d55d6adae9 100644
--- a/test/Core.Test/Services/OrganizationServiceTests.cs
+++ b/test/Core.Test/Services/OrganizationServiceTests.cs
@@ -61,7 +61,7 @@ namespace Bit.Core.Test.Services
.CreateManyAsync(Arg.Is>(users => users.Count() == expectedNewUsersCount));
await sutProvider.GetDependency().Received(1)
.BulkSendOrganizationInviteEmailAsync(org.Name,
- Arg.Is>(messages => messages.Count() == expectedNewUsersCount));
+ Arg.Is>(messages => messages.Count() == expectedNewUsersCount));
// Send events
await sutProvider.GetDependency().Received(1)
@@ -116,7 +116,7 @@ namespace Bit.Core.Test.Services
.CreateManyAsync(Arg.Is>(users => users.Count() == expectedNewUsersCount));
await sutProvider.GetDependency().Received(1)
.BulkSendOrganizationInviteEmailAsync(org.Name,
- Arg.Is>(messages => messages.Count() == expectedNewUsersCount));
+ Arg.Is>(messages => messages.Count() == expectedNewUsersCount));
// Sent events
await sutProvider.GetDependency().Received(1)