1
0
mirror of https://github.com/bitwarden/server synced 2025-12-29 14:43:39 +00:00

[PM-27766] Add policy for blocking account creation from claimed domains. (#6537)

* Add policy for blocking account creation from claimed domains.

* dotnet format

* check as part of email verification

* add feature flag

* fix tests

* try to fix dates on database integration tests

* PR feedback from claude

* remove claude local settings

* pr feedback

* format

* fix test

* create or alter

* PR feedback

* PR feedback

* Update src/Core/Constants.cs

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

* fix merge issues

* fix tests

---------

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
Kyle Spearrin
2025-11-19 20:25:50 -05:00
committed by GitHub
parent 55fb80b2fc
commit c0700a6946
18 changed files with 1502 additions and 18 deletions

View File

@@ -15,16 +15,20 @@ using Bit.Core.Tokens;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Bit.Core.Auth.UserFeatures.Registration.Implementations;
public class RegisterUserCommand : IRegisterUserCommand
{
private readonly ILogger<RegisterUserCommand> _logger;
private readonly IGlobalSettings _globalSettings;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IPolicyRepository _policyRepository;
private readonly IOrganizationDomainRepository _organizationDomainRepository;
private readonly IFeatureService _featureService;
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory;
@@ -37,28 +41,32 @@ public class RegisterUserCommand : IRegisterUserCommand
private readonly IValidateRedemptionTokenCommand _validateRedemptionTokenCommand;
private readonly IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> _emergencyAccessInviteTokenDataFactory;
private readonly IFeatureService _featureService;
private readonly string _disabledUserRegistrationExceptionMsg = "Open registration has been disabled by the system administrator.";
public RegisterUserCommand(
ILogger<RegisterUserCommand> logger,
IGlobalSettings globalSettings,
IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository,
IPolicyRepository policyRepository,
IOrganizationDomainRepository organizationDomainRepository,
IFeatureService featureService,
IDataProtectionProvider dataProtectionProvider,
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> registrationEmailVerificationTokenDataFactory,
IUserService userService,
IMailService mailService,
IValidateRedemptionTokenCommand validateRedemptionTokenCommand,
IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> emergencyAccessInviteTokenDataFactory,
IFeatureService featureService)
IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> emergencyAccessInviteTokenDataFactory)
{
_logger = logger;
_globalSettings = globalSettings;
_organizationUserRepository = organizationUserRepository;
_organizationRepository = organizationRepository;
_policyRepository = policyRepository;
_organizationDomainRepository = organizationDomainRepository;
_featureService = featureService;
_organizationServiceDataProtector = dataProtectionProvider.CreateProtector(
"OrganizationServiceDataProtector");
@@ -77,6 +85,8 @@ public class RegisterUserCommand : IRegisterUserCommand
public async Task<IdentityResult> RegisterUser(User user)
{
await ValidateEmailDomainNotBlockedAsync(user.Email);
var result = await _userService.CreateUserAsync(user);
if (result == IdentityResult.Success)
{
@@ -102,6 +112,11 @@ public class RegisterUserCommand : IRegisterUserCommand
{
TryValidateOrgInviteToken(orgInviteToken, orgUserId, user);
var orgUser = await SetUserEmail2FaIfOrgPolicyEnabledAsync(orgUserId, user);
if (orgUser == null && orgUserId.HasValue)
{
throw new BadRequestException("Invalid organization user invitation.");
}
await ValidateEmailDomainNotBlockedAsync(user.Email, orgUser?.OrganizationId);
user.ApiKey = CoreHelpers.SecureRandomString(30);
@@ -265,6 +280,8 @@ public class RegisterUserCommand : IRegisterUserCommand
string emailVerificationToken)
{
ValidateOpenRegistrationAllowed();
await ValidateEmailDomainNotBlockedAsync(user.Email);
var tokenable = ValidateRegistrationEmailVerificationTokenable(emailVerificationToken, user.Email);
user.EmailVerified = true;
@@ -284,6 +301,7 @@ public class RegisterUserCommand : IRegisterUserCommand
string orgSponsoredFreeFamilyPlanInviteToken)
{
ValidateOpenRegistrationAllowed();
await ValidateEmailDomainNotBlockedAsync(user.Email);
await ValidateOrgSponsoredFreeFamilyPlanInviteToken(orgSponsoredFreeFamilyPlanInviteToken, user.Email);
user.EmailVerified = true;
@@ -304,6 +322,7 @@ public class RegisterUserCommand : IRegisterUserCommand
string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
{
ValidateOpenRegistrationAllowed();
await ValidateEmailDomainNotBlockedAsync(user.Email);
ValidateAcceptEmergencyAccessInviteToken(acceptEmergencyAccessInviteToken, acceptEmergencyAccessId, user.Email);
user.EmailVerified = true;
@@ -322,6 +341,7 @@ public class RegisterUserCommand : IRegisterUserCommand
string providerInviteToken, Guid providerUserId)
{
ValidateOpenRegistrationAllowed();
await ValidateEmailDomainNotBlockedAsync(user.Email);
ValidateProviderInviteToken(providerInviteToken, providerUserId, user.Email);
user.EmailVerified = true;
@@ -387,6 +407,28 @@ public class RegisterUserCommand : IRegisterUserCommand
return tokenable;
}
private async Task ValidateEmailDomainNotBlockedAsync(string email, Guid? excludeOrganizationId = null)
{
// Only check if feature flag is enabled
if (!_featureService.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation))
{
return;
}
var emailDomain = EmailValidation.GetDomain(email);
var isDomainBlocked = await _organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(
emailDomain, excludeOrganizationId);
if (isDomainBlocked)
{
_logger.LogInformation(
"User registration blocked by domain claim policy. Domain: {Domain}, ExcludedOrgId: {ExcludedOrgId}",
emailDomain,
excludeOrganizationId);
throw new BadRequestException("This email address is claimed by an organization using Bitwarden.");
}
}
/// <summary>
/// We send different welcome emails depending on whether the user is joining a free/family or an enterprise organization. If information to populate the
/// email isn't present we send the standard individual welcome email.

View File

@@ -5,6 +5,8 @@ using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Core.Utilities;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Auth.UserFeatures.Registration.Implementations;
@@ -15,25 +17,30 @@ namespace Bit.Core.Auth.UserFeatures.Registration.Implementations;
/// </summary>
public class SendVerificationEmailForRegistrationCommand : ISendVerificationEmailForRegistrationCommand
{
private readonly ILogger<SendVerificationEmailForRegistrationCommand> _logger;
private readonly IUserRepository _userRepository;
private readonly GlobalSettings _globalSettings;
private readonly IMailService _mailService;
private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _tokenDataFactory;
private readonly IFeatureService _featureService;
private readonly IOrganizationDomainRepository _organizationDomainRepository;
public SendVerificationEmailForRegistrationCommand(
ILogger<SendVerificationEmailForRegistrationCommand> logger,
IUserRepository userRepository,
GlobalSettings globalSettings,
IMailService mailService,
IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> tokenDataFactory,
IFeatureService featureService)
IFeatureService featureService,
IOrganizationDomainRepository organizationDomainRepository)
{
_logger = logger;
_userRepository = userRepository;
_globalSettings = globalSettings;
_mailService = mailService;
_tokenDataFactory = tokenDataFactory;
_featureService = featureService;
_organizationDomainRepository = organizationDomainRepository;
}
@@ -49,6 +56,20 @@ public class SendVerificationEmailForRegistrationCommand : ISendVerificationEmai
throw new ArgumentNullException(nameof(email));
}
// Check if the email domain is blocked by an organization policy
if (_featureService.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation))
{
var emailDomain = EmailValidation.GetDomain(email);
if (await _organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(emailDomain))
{
_logger.LogInformation(
"User registration email verification blocked by domain claim policy. Domain: {Domain}",
emailDomain);
throw new BadRequestException("This email address is claimed by an organization using Bitwarden.");
}
}
// Check to see if the user already exists
var user = await _userRepository.GetByEmailAsync(email);
var userExists = user != null;