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 @@ 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)
- You have been invited to join the {{OrganizationName}} organization. + You have been invited to join the {{OrganizationName}} organization. This link expires on {{ExpirationDate}}.