1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-20 12:38:15 -05:00

[PM-19685] Remove email delay feature flag (#5622)

* Remove email delay feature flag

* Fixed reference.

* Removed field from old registration method
This commit is contained in:
Todd Martin 2025-04-07 21:36:08 -04:00 committed by GitHub
parent 91fa02f8e6
commit 8beefbb417
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 7 additions and 32 deletions

View File

@ -53,23 +53,10 @@ public class SendVerificationEmailForRegistrationCommand : ISendVerificationEmai
var user = await _userRepository.GetByEmailAsync(email); var user = await _userRepository.GetByEmailAsync(email);
var userExists = user != null; var userExists = user != null;
// Delays enabled by default; flag must be enabled to remove the delays.
var delaysEnabled = !_featureService.IsEnabled(FeatureFlagKeys.EmailVerificationDisableTimingDelays);
if (!_globalSettings.EnableEmailVerification) if (!_globalSettings.EnableEmailVerification)
{ {
if (userExists) if (userExists)
{ {
if (delaysEnabled)
{
// Add delay to prevent timing attacks
// Note: sub 140 ms feels responsive to users so we are using a random value between 100 - 130 ms
// as it should be long enough to prevent timing attacks but not too long to be noticeable to the user.
await Task.Delay(Random.Shared.Next(100, 130));
}
throw new BadRequestException($"Email {email} is already taken"); throw new BadRequestException($"Email {email} is already taken");
} }
@ -87,11 +74,6 @@ public class SendVerificationEmailForRegistrationCommand : ISendVerificationEmai
await _mailService.SendRegistrationVerificationEmailAsync(email, token); await _mailService.SendRegistrationVerificationEmailAsync(email, token);
} }
if (delaysEnabled)
{
// Add random delay between 100ms-130ms to prevent timing attacks
await Task.Delay(Random.Shared.Next(100, 130));
}
// User exists but we will return a 200 regardless of whether the email was sent or not; so return null // User exists but we will return a 200 regardless of whether the email was sent or not; so return null
return null; return null;
} }

View File

@ -115,7 +115,6 @@ public static class FeatureFlagKeys
public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence";
public const string DuoRedirect = "duo-redirect"; public const string DuoRedirect = "duo-redirect";
public const string EmailVerification = "email-verification"; public const string EmailVerification = "email-verification";
public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays";
public const string DeviceTrustLogging = "pm-8285-device-trust-logging"; public const string DeviceTrustLogging = "pm-8285-device-trust-logging";
public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token"; public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token";
public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh"; public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh";

View File

@ -121,8 +121,7 @@ public class AccountsController : Controller
var user = model.ToUser(); var user = model.ToUser();
var identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(user, model.MasterPasswordHash, var identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(user, model.MasterPasswordHash,
model.Token, model.OrganizationUserId); model.Token, model.OrganizationUserId);
// delaysEnabled false is only for the new registration with email verification process return ProcessRegistrationResult(identityResult, user);
return await ProcessRegistrationResult(identityResult, user, delaysEnabled: true);
} }
[HttpPost("register/send-verification-email")] [HttpPost("register/send-verification-email")]
@ -188,7 +187,6 @@ public class AccountsController : Controller
// Users will either have an emailed token or an email verification token - not both. // Users will either have an emailed token or an email verification token - not both.
IdentityResult identityResult = null; IdentityResult identityResult = null;
var delaysEnabled = !_featureService.IsEnabled(FeatureFlagKeys.EmailVerificationDisableTimingDelays);
switch (model.GetTokenType()) switch (model.GetTokenType())
{ {
@ -197,32 +195,32 @@ public class AccountsController : Controller
await _registerUserCommand.RegisterUserViaEmailVerificationToken(user, model.MasterPasswordHash, await _registerUserCommand.RegisterUserViaEmailVerificationToken(user, model.MasterPasswordHash,
model.EmailVerificationToken); model.EmailVerificationToken);
return await ProcessRegistrationResult(identityResult, user, delaysEnabled); return ProcessRegistrationResult(identityResult, user);
break; break;
case RegisterFinishTokenType.OrganizationInvite: case RegisterFinishTokenType.OrganizationInvite:
identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(user, model.MasterPasswordHash, identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(user, model.MasterPasswordHash,
model.OrgInviteToken, model.OrganizationUserId); model.OrgInviteToken, model.OrganizationUserId);
return await ProcessRegistrationResult(identityResult, user, delaysEnabled); return ProcessRegistrationResult(identityResult, user);
break; break;
case RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan: case RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan:
identityResult = await _registerUserCommand.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, model.MasterPasswordHash, model.OrgSponsoredFreeFamilyPlanToken); identityResult = await _registerUserCommand.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, model.MasterPasswordHash, model.OrgSponsoredFreeFamilyPlanToken);
return await ProcessRegistrationResult(identityResult, user, delaysEnabled); return ProcessRegistrationResult(identityResult, user);
break; break;
case RegisterFinishTokenType.EmergencyAccessInvite: case RegisterFinishTokenType.EmergencyAccessInvite:
Debug.Assert(model.AcceptEmergencyAccessId.HasValue); Debug.Assert(model.AcceptEmergencyAccessId.HasValue);
identityResult = await _registerUserCommand.RegisterUserViaAcceptEmergencyAccessInviteToken(user, model.MasterPasswordHash, identityResult = await _registerUserCommand.RegisterUserViaAcceptEmergencyAccessInviteToken(user, model.MasterPasswordHash,
model.AcceptEmergencyAccessInviteToken, model.AcceptEmergencyAccessId.Value); model.AcceptEmergencyAccessInviteToken, model.AcceptEmergencyAccessId.Value);
return await ProcessRegistrationResult(identityResult, user, delaysEnabled); return ProcessRegistrationResult(identityResult, user);
break; break;
case RegisterFinishTokenType.ProviderInvite: case RegisterFinishTokenType.ProviderInvite:
Debug.Assert(model.ProviderUserId.HasValue); Debug.Assert(model.ProviderUserId.HasValue);
identityResult = await _registerUserCommand.RegisterUserViaProviderInviteToken(user, model.MasterPasswordHash, identityResult = await _registerUserCommand.RegisterUserViaProviderInviteToken(user, model.MasterPasswordHash,
model.ProviderInviteToken, model.ProviderUserId.Value); model.ProviderInviteToken, model.ProviderUserId.Value);
return await ProcessRegistrationResult(identityResult, user, delaysEnabled); return ProcessRegistrationResult(identityResult, user);
break; break;
default: default:
@ -230,7 +228,7 @@ public class AccountsController : Controller
} }
} }
private async Task<RegisterResponseModel> ProcessRegistrationResult(IdentityResult result, User user, bool delaysEnabled) private RegisterResponseModel ProcessRegistrationResult(IdentityResult result, User user)
{ {
if (result.Succeeded) if (result.Succeeded)
{ {
@ -243,10 +241,6 @@ public class AccountsController : Controller
ModelState.AddModelError(string.Empty, error.Description); ModelState.AddModelError(string.Empty, error.Description);
} }
if (delaysEnabled)
{
await Task.Delay(Random.Shared.Next(100, 130));
}
throw new BadRequestException(ModelState); throw new BadRequestException(ModelState);
} }