diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs
index 8a4cd54026..5713341dc4 100644
--- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs
+++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs
@@ -313,7 +313,7 @@ public class OrganizationUsersController : Controller
throw new UnauthorizedAccessException();
}
- await _organizationService.InitPendingOrganization(user.Id, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName);
+ await _organizationService.InitPendingOrganization(user, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName, model.Token);
await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService);
await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id);
}
diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs
index 8d2997bbc6..228c2b522c 100644
--- a/src/Core/AdminConsole/Services/IOrganizationService.cs
+++ b/src/Core/AdminConsole/Services/IOrganizationService.cs
@@ -54,7 +54,7 @@ public interface IOrganizationService
///
/// This method must target a disabled Organization that has null keys and status as 'Pending'.
///
- Task InitPendingOrganization(Guid userId, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName);
+ Task InitPendingOrganization(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken);
Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null);
void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade);
diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs
index c9b38b3e30..b31b43406e 100644
--- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs
+++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs
@@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums;
+using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Constants;
@@ -30,10 +31,12 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Settings;
+using Bit.Core.Tokens;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
+using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Logging;
using Stripe;
using OrganizationUserInvite = Bit.Core.Models.Business.OrganizationUserInvite;
@@ -74,6 +77,8 @@ public class OrganizationService : IOrganizationService
private readonly IPricingClient _pricingClient;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand;
+ private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory;
+ private readonly IDataProtector _dataProtector;
public OrganizationService(
IOrganizationRepository organizationRepository,
@@ -107,7 +112,10 @@ public class OrganizationService : IOrganizationService
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IPricingClient pricingClient,
IPolicyRequirementQuery policyRequirementQuery,
- ISendOrganizationInvitesCommand sendOrganizationInvitesCommand)
+ ISendOrganizationInvitesCommand sendOrganizationInvitesCommand,
+ IDataProtectorTokenFactory orgUserInviteTokenDataFactory,
+ IDataProtectionProvider dataProtectionProvider
+ )
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@@ -141,6 +149,8 @@ public class OrganizationService : IOrganizationService
_pricingClient = pricingClient;
_policyRequirementQuery = policyRequirementQuery;
_sendOrganizationInvitesCommand = sendOrganizationInvitesCommand;
+ _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
+ _dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose);
}
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
@@ -1912,9 +1922,28 @@ public class OrganizationService : IOrganizationService
});
}
- public async Task InitPendingOrganization(Guid userId, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName)
+ public async Task InitPendingOrganization(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken)
{
- await ValidateSignUpPoliciesAsync(userId);
+ await ValidateSignUpPoliciesAsync(user.Id);
+
+ var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
+ if (orgUser == null)
+ {
+ throw new BadRequestException("User invalid.");
+ }
+
+ // TODO: PM-4142 - remove old token validation logic once 3 releases of backwards compatibility are complete
+ var newTokenValid = OrgUserInviteTokenable.ValidateOrgUserInviteStringToken(
+ _orgUserInviteTokenDataFactory, emailToken, orgUser);
+
+ var tokenValid = newTokenValid ||
+ CoreHelpers.UserInviteTokenIsValid(_dataProtector, emailToken, user.Email, orgUser.Id,
+ _globalSettings);
+
+ if (!tokenValid)
+ {
+ throw new BadRequestException("Invalid token.");
+ }
var org = await GetOrgById(organizationId);