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:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user