1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-05 18:12:48 -05:00

[PM-5093][PM-7325] Added trial initiation email verification endpoint (#4221)

* Added trial initiation user verification endpoint

* Added explanatory comment for why we add artificial delay

* Updated RegistrationStart to Registration reference event

* Ensure that productTier query param is an int

* Added email value to trial initiation email
This commit is contained in:
Conner Turnbull
2024-07-29 14:18:12 -04:00
committed by GitHub
parent 2b738a5a4c
commit 656e0c20f9
12 changed files with 266 additions and 1 deletions

View File

@ -0,0 +1,10 @@
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Billing.Enums;
namespace Bit.Core.Billing.Models.Api.Requests.Accounts;
public class TrialSendVerificationEmailRequestModel : RegisterSendVerificationEmailRequestModel
{
public ProductTierType ProductTier { get; set; }
public IEnumerable<ProductType> Products { get; set; }
}

View File

@ -0,0 +1,33 @@
using Bit.Core.Auth.Models.Mail;
using Bit.Core.Billing.Enums;
namespace Bit.Core.Billing.Models.Mail;
public class TrialInitiationVerifyEmail : RegisterVerifyEmail
{
/// <summary>
/// See comment on <see cref="RegisterVerifyEmail"/>.<see cref="RegisterVerifyEmail.Url"/>
/// </summary>
public new string Url
{
get => $"{WebVaultUrl}/{Route}" +
$"?token={Token}" +
$"&email={Email}" +
$"&fromEmail=true" +
$"&productTier={(int)ProductTier}" +
$"&product={string.Join(",", Product.Select(p => (int)p))}";
}
public ProductTierType ProductTier { get; set; }
public IEnumerable<ProductType> Product { get; set; }
/// <summary>
/// Currently we only support one product type at a time, despite Product being a collection.
/// If we receive both PasswordManager and SecretsManager, we'll send the user to the PM trial route
/// </summary>
private string Route =>
Product.Any(p => p == ProductType.PasswordManager)
? "trial-initiation"
: "secrets-manager-trial-initiation";
}

View File

@ -0,0 +1,14 @@
#nullable enable
using Bit.Core.Billing.Enums;
namespace Bit.Core.Billing.TrialInitiation.Registration;
public interface ISendTrialInitiationEmailForRegistrationCommand
{
public Task<string?> Handle(
string email,
string? name,
bool receiveMarketingEmails,
ProductTierType productTier,
IEnumerable<ProductType> products);
}

View File

@ -0,0 +1,74 @@
#nullable enable
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Billing.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Core.Utilities;
namespace Bit.Core.Billing.TrialInitiation.Registration.Implementations;
public class SendTrialInitiationEmailForRegistrationCommand(
IUserRepository userRepository,
GlobalSettings globalSettings,
IMailService mailService,
IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> tokenDataFactory)
: ISendTrialInitiationEmailForRegistrationCommand
{
public async Task<string?> Handle(
string email,
string? name,
bool receiveMarketingEmails,
ProductTierType productTier,
IEnumerable<ProductType> products)
{
ArgumentException.ThrowIfNullOrWhiteSpace(email, nameof(email));
var userExists = await CheckUserExistsConstantTimeAsync(email);
var token = GenerateToken(email, name, receiveMarketingEmails);
if (!globalSettings.EnableEmailVerification)
{
await PerformConstantTimeOperationsAsync();
if (userExists)
{
throw new BadRequestException($"Email {email} is already taken");
}
return token;
}
await PerformConstantTimeOperationsAsync();
if (!userExists)
{
await mailService.SendTrialInitiationSignupEmailAsync(email, token, productTier, products);
}
return null;
}
/// <summary>
/// Perform constant time operations to prevent timing attacks
/// </summary>
private static async Task PerformConstantTimeOperationsAsync()
{
await Task.Delay(130);
}
private string GenerateToken(string email, string? name, bool receiveMarketingEmails)
{
var tokenable = new RegistrationEmailVerificationTokenable(email, name, receiveMarketingEmails);
return tokenDataFactory.Protect(tokenable);
}
private async Task<bool> CheckUserExistsConstantTimeAsync(string email)
{
var user = await userRepository.GetByEmailAsync(email);
return CoreHelpers.FixedTimeEquals(user?.Email.ToLowerInvariant() ?? string.Empty, email.ToLowerInvariant());
}
}

View File

@ -0,0 +1,13 @@
using Bit.Core.Billing.TrialInitiation.Registration;
using Bit.Core.Billing.TrialInitiation.Registration.Implementations;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.Billing.TrialInitiation;
public static class TrialInitiationCollectionExtensions
{
public static void AddTrialInitiationServices(this IServiceCollection services)
{
services.AddSingleton<ISendTrialInitiationEmailForRegistrationCommand, SendTrialInitiationEmailForRegistrationCommand>();
}
}