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:
@ -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; }
|
||||
}
|
33
src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs
Normal file
33
src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs
Normal 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";
|
||||
}
|
@ -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);
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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>();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user