From f678e3db797d78a8f69dd49cfbd4be224136d386 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Tue, 15 Apr 2025 15:39:21 -0400 Subject: [PATCH] [PM-19887] authorization for init pending organization (#5643) * add token authorization for initPendingOrganizations * clean up --- .../OrganizationUsersController.cs | 2 +- .../Services/IOrganizationService.cs | 2 +- .../Implementations/OrganizationService.cs | 35 +++++++++++++++++-- 3 files changed, 34 insertions(+), 5 deletions(-) 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);