mirror of
https://github.com/bitwarden/server
synced 2025-12-28 06:03:29 +00:00
fix(2fa): [PM-22323] Do not show 2FA warning for 2FA setup and login emails
* Added configuration to not display 2FA setup instruction * Refactored to new service. * Linting. * Dependency injection * Changed to scoped to have access to ICurrentContext. * Inverted logic for EmailTotpAction * Fixed tests. * Fixed tests. * More tests. * Fixed tests. * Linting. * Added tests at controller level. * Linting * Fixed error in test. * Review updates. * Accidentally deleted imports.
This commit is contained in:
8
src/Core/Auth/Enums/TwoFactorEmailPurpose.cs
Normal file
8
src/Core/Auth/Enums/TwoFactorEmailPurpose.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Core.Auth.Enums;
|
||||
|
||||
public enum TwoFactorEmailPurpose
|
||||
{
|
||||
Login,
|
||||
Setup,
|
||||
NewDeviceVerification,
|
||||
}
|
||||
11
src/Core/Auth/Services/ITwoFactorEmailService.cs
Normal file
11
src/Core/Auth/Services/ITwoFactorEmailService.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.Auth.Services;
|
||||
|
||||
public interface ITwoFactorEmailService
|
||||
{
|
||||
Task SendTwoFactorEmailAsync(User user);
|
||||
Task SendTwoFactorSetupEmailAsync(User user);
|
||||
Task SendNewDeviceVerificationEmailAsync(User user);
|
||||
Task<bool> VerifyTwoFactorTokenAsync(User user, string token);
|
||||
}
|
||||
116
src/Core/Auth/Services/Implementations/TwoFactorEmailService.cs
Normal file
116
src/Core/Auth/Services/Implementations/TwoFactorEmailService.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Core.Auth.Enums;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.Auth.Services;
|
||||
|
||||
public class TwoFactorEmailService : ITwoFactorEmailService
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly UserManager<User> _userManager;
|
||||
private readonly IMailService _mailService;
|
||||
|
||||
public TwoFactorEmailService(
|
||||
ICurrentContext currentContext,
|
||||
IMailService mailService,
|
||||
UserManager<User> userManager
|
||||
)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_userManager = userManager;
|
||||
_mailService = mailService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a two-factor email to the user with an OTP token for login
|
||||
/// </summary>
|
||||
/// <param name="user">The user to whom the email should be sent</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown if the user does not have an email for email 2FA</exception>
|
||||
public async Task SendTwoFactorEmailAsync(User user)
|
||||
{
|
||||
await VerifyAndSendTwoFactorEmailAsync(user, TwoFactorEmailPurpose.Login);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a two-factor email to the user with an OTP for setting up 2FA
|
||||
/// </summary>
|
||||
/// <param name="user">The user to whom the email should be sent</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown if the user does not have an email for email 2FA</exception>
|
||||
public async Task SendTwoFactorSetupEmailAsync(User user)
|
||||
{
|
||||
await VerifyAndSendTwoFactorEmailAsync(user, TwoFactorEmailPurpose.Setup);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a new device verification email to the user with an OTP token
|
||||
/// </summary>
|
||||
/// <param name="user">The user to whom the email should be sent</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown if the user is not provided</exception>
|
||||
public async Task SendNewDeviceVerificationEmailAsync(User user)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
|
||||
var token = await _userManager.GenerateUserTokenAsync(user, TokenOptions.DefaultEmailProvider,
|
||||
"otp:" + user.Email);
|
||||
|
||||
var deviceType = _currentContext.DeviceType?.GetType().GetMember(_currentContext.DeviceType?.ToString())
|
||||
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName() ?? "Unknown Browser";
|
||||
|
||||
await _mailService.SendTwoFactorEmailAsync(
|
||||
user.Email, user.Email, token, _currentContext.IpAddress, deviceType, TwoFactorEmailPurpose.NewDeviceVerification);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the two-factor token for the specified user
|
||||
/// </summary>
|
||||
/// <param name="user">The user for whom the token should be verified</param>
|
||||
/// <param name="token">The token to verify</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown if the user does not have an email for email 2FA</exception>
|
||||
public async Task<bool> VerifyTwoFactorTokenAsync(User user, string token)
|
||||
{
|
||||
var email = GetUserTwoFactorEmail(user);
|
||||
return await _userManager.VerifyTwoFactorTokenAsync(user,
|
||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a two-factor email with the specified purpose to the user only if they have 2FA email set up
|
||||
/// </summary>
|
||||
/// <param name="user">The user to whom the email should be sent</param>
|
||||
/// <param name="purpose">The purpose of the email</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown if the user does not have an email set up for 2FA</exception>
|
||||
private async Task VerifyAndSendTwoFactorEmailAsync(User user, TwoFactorEmailPurpose purpose)
|
||||
{
|
||||
var email = GetUserTwoFactorEmail(user);
|
||||
var token = await _userManager.GenerateTwoFactorTokenAsync(user,
|
||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.Email));
|
||||
|
||||
var deviceType = _currentContext.DeviceType?.GetType().GetMember(_currentContext.DeviceType?.ToString())
|
||||
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName() ?? "Unknown Browser";
|
||||
|
||||
await _mailService.SendTwoFactorEmailAsync(
|
||||
email, user.Email, token, _currentContext.IpAddress, deviceType, purpose);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the user has email 2FA and will return the email if present and throw otherwise.
|
||||
/// </summary>
|
||||
/// <param name="user">The user to check</param>
|
||||
/// <returns>The user's 2FA email address</returns>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
private string GetUserTwoFactorEmail(User user)
|
||||
{
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email);
|
||||
if (provider == null || provider.MetaData == null || !provider.MetaData.TryGetValue("Email", out var emailValue))
|
||||
{
|
||||
throw new ArgumentNullException("No email.");
|
||||
}
|
||||
return ((string)emailValue).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,9 @@
|
||||
<ul>
|
||||
<li>Deauthorize unrecognized devices</li>
|
||||
<li>Change your master password</li>
|
||||
<li>Turn on two-step login</li>
|
||||
{{#if DisplayTwoFactorReminder}}
|
||||
<li style="margin-bottom: 5px;">Turn on two-step login</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -22,4 +22,9 @@ public class TwoFactorEmailTokenViewModel : BaseMailModel
|
||||
public string TimeZone { get; set; }
|
||||
public string DeviceIp { get; set; }
|
||||
public string DeviceType { get; set; }
|
||||
/// <summary>
|
||||
/// Depending on the context, we may want to show a reminder to the user that they should enable two factor authentication.
|
||||
/// This is not relevant when the user is using the email to verify setting up 2FA, so we hide it in that case.
|
||||
/// </summary>
|
||||
public bool DisplayTwoFactorReminder { get; set; }
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Models.Mail;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
using Core.Auth.Enums;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
@@ -27,7 +28,7 @@ public interface IMailService
|
||||
Task SendCannotDeleteClaimedAccountEmailAsync(string email);
|
||||
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
|
||||
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
|
||||
Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, bool authentication = true);
|
||||
Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose);
|
||||
Task SendNoMasterPasswordHintEmailAsync(string email);
|
||||
Task SendMasterPasswordHintEmailAsync(string email, string hint);
|
||||
|
||||
|
||||
@@ -21,21 +21,6 @@ public interface IUserService
|
||||
Task<IdentityResult> CreateUserAsync(User user);
|
||||
Task<IdentityResult> CreateUserAsync(User user, string masterPasswordHash);
|
||||
Task SendMasterPasswordHintAsync(string email);
|
||||
/// <summary>
|
||||
/// Used for both email two factor and email two factor setup.
|
||||
/// </summary>
|
||||
/// <param name="user">user requesting the action</param>
|
||||
/// <param name="authentication">this controls if what verbiage is shown in the email</param>
|
||||
/// <returns>void</returns>
|
||||
Task SendTwoFactorEmailAsync(User user, bool authentication = true);
|
||||
/// <summary>
|
||||
/// Calls the same email implementation but instead it sends the token to the account email not the
|
||||
/// email set up for two-factor, since in practice they can be different.
|
||||
/// </summary>
|
||||
/// <param name="user">user attepting to login with a new device</param>
|
||||
/// <returns>void</returns>
|
||||
Task SendNewDeviceVerificationEmailAsync(User user);
|
||||
Task<bool> VerifyTwoFactorEmailAsync(User user, string token);
|
||||
Task<CredentialCreateOptions> StartWebAuthnRegistrationAsync(User user);
|
||||
Task<bool> DeleteWebAuthnKeyAsync(User user, int id);
|
||||
Task<bool> CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse);
|
||||
@@ -87,7 +72,6 @@ public interface IUserService
|
||||
Task SendOTPAsync(User user);
|
||||
Task<bool> VerifyOTPAsync(User user, string token);
|
||||
Task<bool> VerifySecretAsync(User user, string secret, bool isSettingMFA = false);
|
||||
Task ResendNewDeviceVerificationEmail(string email, string secret);
|
||||
/// <summary>
|
||||
/// We use this method to check if the user has an active new device verification bypass
|
||||
/// </summary>
|
||||
|
||||
@@ -21,6 +21,7 @@ using Bit.Core.SecretsManager.Models.Mail;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
using Core.Auth.Enums;
|
||||
using HandlebarsDotNet;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
@@ -166,14 +167,14 @@ public class HandlebarsMailService : IMailService
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, bool authentication = true)
|
||||
public async Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose)
|
||||
{
|
||||
var message = CreateDefaultMessage("Your Bitwarden Verification Code", email);
|
||||
var requestDateTime = DateTime.UtcNow;
|
||||
var model = new TwoFactorEmailTokenViewModel
|
||||
{
|
||||
Token = token,
|
||||
EmailTotpAction = authentication ? "logging in" : "setting up two-step login",
|
||||
EmailTotpAction = (purpose == TwoFactorEmailPurpose.Setup) ? "setting up two-step login" : "logging in",
|
||||
AccountEmail = accountEmail,
|
||||
TheDate = requestDateTime.ToLongDateString(),
|
||||
TheTime = requestDateTime.ToShortTimeString(),
|
||||
@@ -182,6 +183,9 @@ public class HandlebarsMailService : IMailService
|
||||
DeviceType = deviceType,
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName,
|
||||
// We only want to remind users to set up 2FA if they're getting a new device verification email.
|
||||
// For login with 2FA, and setup of 2FA, we do not want to show the reminder because users are already doing so.
|
||||
DisplayTwoFactorReminder = purpose == TwoFactorEmailPurpose.NewDeviceVerification
|
||||
};
|
||||
await AddMessageContentAsync(message, "Auth.TwoFactorEmail", model);
|
||||
message.MetaData.Add("SendGridBypassListManagement", true);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
@@ -337,52 +335,6 @@ public class UserService : UserManager<User>, IUserService
|
||||
await _mailService.SendMasterPasswordHintEmailAsync(email, user.MasterPasswordHint);
|
||||
}
|
||||
|
||||
public async Task SendTwoFactorEmailAsync(User user, bool authentication = true)
|
||||
{
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email);
|
||||
if (provider == null || provider.MetaData == null || !provider.MetaData.TryGetValue("Email", out var emailValue))
|
||||
{
|
||||
throw new ArgumentNullException("No email.");
|
||||
}
|
||||
|
||||
var email = ((string)emailValue).ToLowerInvariant();
|
||||
var token = await base.GenerateTwoFactorTokenAsync(user,
|
||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.Email));
|
||||
|
||||
var deviceType = _currentContext.DeviceType?.GetType().GetMember(_currentContext.DeviceType?.ToString())
|
||||
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName() ?? "Unknown Browser";
|
||||
|
||||
await _mailService.SendTwoFactorEmailAsync(
|
||||
email, user.Email, token, _currentContext.IpAddress, deviceType, authentication);
|
||||
}
|
||||
|
||||
public async Task SendNewDeviceVerificationEmailAsync(User user)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
|
||||
var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultEmailProvider,
|
||||
"otp:" + user.Email);
|
||||
|
||||
var deviceType = _currentContext.DeviceType?.GetType().GetMember(_currentContext.DeviceType?.ToString())
|
||||
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName() ?? "Unknown Browser";
|
||||
|
||||
await _mailService.SendTwoFactorEmailAsync(
|
||||
user.Email, user.Email, token, _currentContext.IpAddress, deviceType);
|
||||
}
|
||||
|
||||
public async Task<bool> VerifyTwoFactorEmailAsync(User user, string token)
|
||||
{
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email);
|
||||
if (provider == null || provider.MetaData == null || !provider.MetaData.TryGetValue("Email", out var emailValue))
|
||||
{
|
||||
throw new ArgumentNullException("No email.");
|
||||
}
|
||||
|
||||
var email = ((string)emailValue).ToLowerInvariant();
|
||||
return await base.VerifyTwoFactorTokenAsync(user,
|
||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), token);
|
||||
}
|
||||
|
||||
public async Task<CredentialCreateOptions> StartWebAuthnRegistrationAsync(User user)
|
||||
{
|
||||
var providers = user.GetTwoFactorProviders();
|
||||
@@ -1454,20 +1406,6 @@ public class UserService : UserManager<User>, IUserService
|
||||
return isVerified;
|
||||
}
|
||||
|
||||
public async Task ResendNewDeviceVerificationEmail(string email, string secret)
|
||||
{
|
||||
var user = await _userRepository.GetByEmailAsync(email);
|
||||
if (user == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (await VerifySecretAsync(user, secret))
|
||||
{
|
||||
await SendNewDeviceVerificationEmailAsync(user);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ActiveNewDeviceVerificationException(Guid userId)
|
||||
{
|
||||
var cacheKey = string.Format(AuthConstants.NewDeviceVerificationExceptionCacheKeyFormat, userId.ToString());
|
||||
|
||||
@@ -8,6 +8,7 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Models.Mail;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
using Core.Auth.Enums;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
@@ -86,7 +87,7 @@ public class NoopMailService : IMailService
|
||||
public Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email) =>
|
||||
Task.CompletedTask;
|
||||
|
||||
public Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, bool authentication = true)
|
||||
public Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user