mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42:48 -05:00
[PM-17645] : update email for new email multi factor tokens (#5428)
* feat(newDeviceVerification) : Initial update to email * fix : email copying over extra whitespace when using keyboard short cuts * test : Fixing tests for new device verificaiton email format
This commit is contained in:
@ -288,12 +288,17 @@ public class TwoFactorController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This endpoint is only used to set-up email two factor authentication.
|
||||
/// </summary>
|
||||
/// <param name="model">secret verification model</param>
|
||||
/// <returns>void</returns>
|
||||
[HttpPost("send-email")]
|
||||
public async Task SendEmail([FromBody] TwoFactorEmailRequestModel model)
|
||||
{
|
||||
var user = await CheckAsync(model, false, true);
|
||||
model.ToUser(user);
|
||||
await _userService.SendTwoFactorEmailAsync(user);
|
||||
await _userService.SendTwoFactorEmailAsync(user, false);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
|
@ -1,14 +1,38 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||
Your two-step verification code is: <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{Token}}</b>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
||||
Use this code to complete logging in with Bitwarden.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
||||
To finish {{EmailTotpAction}}, enter this verification code: <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{Token}}</b>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
||||
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
If this was not you, take these immediate steps to secure your account in the <a target="_blank" clicktracking="off" href="{{{WebVaultUrl}}}" style="-webkit-font-smoothing: antialiased;-webkit-text-size-adjust: none;box-sizing: border-box;color: #175ddc;font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size: 16px;line-height: 25px;margin: 0;text-decoration: none;">web app</a>:
|
||||
<ul>
|
||||
<li>Deauthorize unrecognized devices</li>
|
||||
<li>Change your master password</li>
|
||||
<li>Turn on two-step login</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
||||
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
<hr />
|
||||
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Account:</b>
|
||||
{{AccountEmail}}
|
||||
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Date:</b>
|
||||
{{TheDate}} at {{TheTime}} {{TimeZone}}
|
||||
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">IP:</b>
|
||||
{{DeviceIp}}
|
||||
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">DeviceType:</b>
|
||||
{{DeviceType}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
||||
{{/FullHtmlLayout}}
|
@ -1,5 +1,16 @@
|
||||
{{#>BasicTextLayout}}
|
||||
Your two-step verification code is: {{Token}}
|
||||
To finish {{EmailTotpAction}}, enter this verification code: {{Token}}
|
||||
|
||||
Use this code to complete logging in with Bitwarden.
|
||||
If this was not you, take these immediate steps to secure your account in the web app:
|
||||
|
||||
Deauthorize unrecognized devices
|
||||
|
||||
Change your master password
|
||||
|
||||
Turn on two-step login
|
||||
|
||||
Account : {{AccountEmail}}
|
||||
Date : {{TheDate}} at {{TheTime}} {{TimeZone}}
|
||||
IP : {{DeviceIp}}
|
||||
Device Type : {{DeviceType}}
|
||||
{{/BasicTextLayout}}
|
25
src/Core/Models/Mail/TwoFactorEmailTokenViewModel.cs
Normal file
25
src/Core/Models/Mail/TwoFactorEmailTokenViewModel.cs
Normal file
@ -0,0 +1,25 @@
|
||||
namespace Bit.Core.Models.Mail;
|
||||
|
||||
/// <summary>
|
||||
/// This view model is used to set-up email two factor authentication, to log in with email two factor authentication,
|
||||
/// and for new device verification.
|
||||
/// </summary>
|
||||
public class TwoFactorEmailTokenViewModel : BaseMailModel
|
||||
{
|
||||
public string Token { get; set; }
|
||||
/// <summary>
|
||||
/// This view model is used to also set-up email two factor authentication. We use this property to communicate
|
||||
/// the purpose of the email, since it can be used for logging in and for setting up.
|
||||
/// </summary>
|
||||
public string EmailTotpAction { get; set; }
|
||||
/// <summary>
|
||||
/// When logging in with email two factor the account email may not be the same as the email used for two factor.
|
||||
/// we want to show the account email in the email, so the user knows which account they are logging into.
|
||||
/// </summary>
|
||||
public string AccountEmail { get; set; }
|
||||
public string TheDate { get; set; }
|
||||
public string TheTime { get; set; }
|
||||
public string TimeZone { get; set; }
|
||||
public string DeviceIp { get; set; }
|
||||
public string DeviceType { get; set; }
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
namespace Bit.Core.Models.Mail;
|
||||
|
||||
public class EmailTokenViewModel : BaseMailModel
|
||||
public class UserVerificationEmailTokenViewModel : BaseMailModel
|
||||
{
|
||||
public string Token { get; set; }
|
||||
}
|
@ -23,7 +23,7 @@ public interface IMailService
|
||||
Task SendCannotDeleteManagedAccountEmailAsync(string email);
|
||||
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
|
||||
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
|
||||
Task SendTwoFactorEmailAsync(string email, string token);
|
||||
Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, bool authentication = true);
|
||||
Task SendNoMasterPasswordHintEmailAsync(string email);
|
||||
Task SendMasterPasswordHintEmailAsync(string email, string hint);
|
||||
|
||||
|
@ -21,7 +21,21 @@ public interface IUserService
|
||||
Task<IdentityResult> CreateUserAsync(User user);
|
||||
Task<IdentityResult> CreateUserAsync(User user, string masterPasswordHash);
|
||||
Task SendMasterPasswordHintAsync(string email);
|
||||
Task SendTwoFactorEmailAsync(User user);
|
||||
/// <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);
|
||||
|
@ -146,7 +146,7 @@ public class HandlebarsMailService : IMailService
|
||||
public async Task SendChangeEmailEmailAsync(string newEmailAddress, string token)
|
||||
{
|
||||
var message = CreateDefaultMessage("Your Email Change", newEmailAddress);
|
||||
var model = new EmailTokenViewModel
|
||||
var model = new UserVerificationEmailTokenViewModel
|
||||
{
|
||||
Token = token,
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
@ -158,14 +158,22 @@ public class HandlebarsMailService : IMailService
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendTwoFactorEmailAsync(string email, string token)
|
||||
public async Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, bool authentication = true)
|
||||
{
|
||||
var message = CreateDefaultMessage("Your Two-step Login Verification Code", email);
|
||||
var model = new EmailTokenViewModel
|
||||
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",
|
||||
AccountEmail = accountEmail,
|
||||
TheDate = requestDateTime.ToLongDateString(),
|
||||
TheTime = requestDateTime.ToShortTimeString(),
|
||||
TimeZone = _utcTimeZoneDisplay,
|
||||
DeviceIp = deviceIp,
|
||||
DeviceType = deviceType,
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName
|
||||
SiteName = _globalSettings.SiteName,
|
||||
};
|
||||
await AddMessageContentAsync(message, "Auth.TwoFactorEmail", model);
|
||||
message.MetaData.Add("SendGridBypassListManagement", true);
|
||||
@ -1012,7 +1020,7 @@ public class HandlebarsMailService : IMailService
|
||||
public async Task SendOTPEmailAsync(string email, string token)
|
||||
{
|
||||
var message = CreateDefaultMessage("Your Bitwarden Verification Code", email);
|
||||
var model = new EmailTokenViewModel
|
||||
var model = new UserVerificationEmailTokenViewModel
|
||||
{
|
||||
Token = token,
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
|
@ -1,4 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
@ -350,7 +352,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
await _mailService.SendMasterPasswordHintEmailAsync(email, user.MasterPasswordHint);
|
||||
}
|
||||
|
||||
public async Task SendTwoFactorEmailAsync(User user)
|
||||
public async Task SendTwoFactorEmailAsync(User user, bool authentication = true)
|
||||
{
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email);
|
||||
if (provider == null || provider.MetaData == null || !provider.MetaData.ContainsKey("Email"))
|
||||
@ -361,7 +363,26 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
var email = ((string)provider.MetaData["Email"]).ToLowerInvariant();
|
||||
var token = await base.GenerateTwoFactorTokenAsync(user,
|
||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.Email));
|
||||
await _mailService.SendTwoFactorEmailAsync(email, token);
|
||||
|
||||
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)
|
||||
@ -1519,7 +1540,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
|
||||
if (await VerifySecretAsync(user, secret))
|
||||
{
|
||||
await SendOTPAsync(user);
|
||||
await SendNewDeviceVerificationEmailAsync(user);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,7 +87,7 @@ public class NoopMailService : IMailService
|
||||
public Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email) =>
|
||||
Task.CompletedTask;
|
||||
|
||||
public Task SendTwoFactorEmailAsync(string email, string token)
|
||||
public Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, bool authentication = true)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ public class DeviceValidator(
|
||||
BuildDeviceErrorResult(validationResult);
|
||||
if (validationResult == DeviceValidationResultType.NewDeviceVerificationRequired)
|
||||
{
|
||||
await _userService.SendOTPAsync(context.User);
|
||||
await _userService.SendNewDeviceVerificationEmailAsync(context.User);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -163,6 +163,14 @@ public class DeviceValidator(
|
||||
return DeviceValidationResultType.NewDeviceVerificationRequired;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends an email whenever the user logs in from a new device. Will not send to a user who's account
|
||||
/// is less than 10 minutes old. We assume an account that is less than 10 minutes old is new and does
|
||||
/// not need an email stating they just logged in.
|
||||
/// </summary>
|
||||
/// <param name="user">user logging in</param>
|
||||
/// <param name="requestDevice">current device being approved to login</param>
|
||||
/// <returns>void</returns>
|
||||
private async Task SendNewDeviceLoginEmail(User user, Device requestDevice)
|
||||
{
|
||||
// Ensure that the user doesn't receive a "new device" email on the first login
|
||||
|
Reference in New Issue
Block a user