1
0
mirror of https://github.com/bitwarden/server synced 2026-01-04 09:33:40 +00:00

[PM-22678] Send email otp authentication method (#6255)

feat(auth): email OTP validation, and generalize authentication interface

- Generalized send authentication method interface
- Made validate method async
- Added email mail support for Handlebars
- Modified email templates to match future implementation

fix(auth): update constants, naming conventions, and error handling

- Renamed constants for clarity
- Updated claims naming convention
- Fixed error message generation
- Added customResponse for Rust consumption

test(auth): add and fix tests for validators and email

- Added tests for SendEmailOtpRequestValidator
- Updated tests for SendAccessGrantValidator

chore: apply dotnet formatting
This commit is contained in:
Ike
2025-09-02 16:48:57 -04:00
committed by GitHub
parent a5bed5dcaa
commit d2d3e0f11b
24 changed files with 1213 additions and 90 deletions

View File

@@ -9,12 +9,12 @@ public static class SendAccessClaimsPrincipalExtensions
{
ArgumentNullException.ThrowIfNull(user);
var sendIdClaim = user.FindFirst(Claims.SendId)
?? throw new InvalidOperationException("Send ID claim not found.");
var sendIdClaim = user.FindFirst(Claims.SendAccessClaims.SendId)
?? throw new InvalidOperationException("send_id claim not found.");
if (!Guid.TryParse(sendIdClaim.Value, out var sendGuid))
{
throw new InvalidOperationException("Invalid Send ID claim value.");
throw new InvalidOperationException("Invalid send_id claim value.");
}
return sendGuid;

View File

@@ -39,6 +39,9 @@ public static class Claims
public const string ManageResetPassword = "manageresetpassword";
public const string ManageScim = "managescim";
}
public const string SendId = "send_id";
public static class SendAccessClaims
{
public const string SendId = "send_id";
public const string Email = "send_email";
}
}

View File

@@ -0,0 +1,28 @@
{{#>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 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">
Verify your email to access this Bitwarden Send.
</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;" />
Your 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">
<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;" />
This code can only be used once and expires in 5 minutes. After that you'll need to verify your email again.
</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 />
{{TheDate}} at {{TheTime}} {{TimeZone}}
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@@ -0,0 +1,9 @@
{{#>BasicTextLayout}}
Verify your email to access this Bitwarden Send.
Your verification code is: {{Token}}
This code can only be used once and expires in 5 minutes. After that you'll need to verify your email again.
Date : {{TheDate}} at {{TheTime}} {{TimeZone}}
{{/BasicTextLayout}}

View File

@@ -0,0 +1,12 @@
namespace Bit.Core.Models.Mail.Auth;
/// <summary>
/// Send email OTP view model
/// </summary>
public class DefaultEmailOtpViewModel : BaseMailModel
{
public string? Token { get; set; }
public string? TheDate { get; set; }
public string? TheTime { get; set; }
public string? TimeZone { get; set; }
}

View File

@@ -30,6 +30,7 @@ public interface IMailService
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose);
Task SendSendEmailOtpEmailAsync(string email, string token, string subject);
Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType type, DateTime utcNow, string ip);
Task SendNoMasterPasswordHintEmailAsync(string email);
Task SendMasterPasswordHintEmailAsync(string email, string hint);

View File

@@ -15,6 +15,7 @@ using Bit.Core.Billing.Models.Mail;
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Mail;
using Bit.Core.Models.Mail.Auth;
using Bit.Core.Models.Mail.Billing;
using Bit.Core.Models.Mail.FamiliesForEnterprise;
using Bit.Core.Models.Mail.Provider;
@@ -199,6 +200,26 @@ public class HandlebarsMailService : IMailService
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendSendEmailOtpEmailAsync(string email, string token, string subject)
{
var message = CreateDefaultMessage(subject, email);
var requestDateTime = DateTime.UtcNow;
var model = new DefaultEmailOtpViewModel
{
Token = token,
TheDate = requestDateTime.ToLongDateString(),
TheTime = requestDateTime.ToShortTimeString(),
TimeZone = _utcTimeZoneDisplay,
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
};
await AddMessageContentAsync(message, "Auth.SendAccessEmailOtpEmail", model);
message.MetaData.Add("SendGridBypassListManagement", true);
// TODO - PM-25380 change to string constant
message.Category = "SendEmailOtp";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip)
{
// Check if we've sent this email within the last hour

View File

@@ -93,6 +93,11 @@ public class NoopMailService : IMailService
return Task.FromResult(0);
}
public Task SendSendEmailOtpEmailAsync(string email, string token, string subject)
{
return Task.FromResult(0);
}
public Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip)
{
return Task.FromResult(0);