mirror of
https://github.com/bitwarden/server
synced 2025-12-15 07:43:54 +00:00
Auth/PM-7322 - Registration with Email verification - Finish registration endpoint (#4182)
* PM-7322 - AccountsController.cs - create empty method + empty req model to be able to create draft PR. * PM-7322 - Start on RegisterFinishRequestModel.cs * PM-7322 - WIP on Complete Registration endpoint * PM-7322 - UserService.cs - RegisterUserAsync - Tweak of token to be orgInviteToken as we are adding a new email verification token to the mix. * PM-7322 - UserService - Rename MP to MPHash * PM-7322 - More WIP progress on getting new finish registration process in place. * PM-7322 Create IRegisterUserCommand * PM-7322 - RegisterUserCommand.cs - first WIP draft * PM-7322 - Implement use of new command in Identity. * PM-7322 - Rename RegisterUserViaOrgInvite to just be RegisterUser as orgInvite is optional. * PM07322 - Test RegisterUserCommand.RegisterUser(...) happy paths and one bad request path. * PM-7322 - More WIP on RegisterUserCommand.cs and tests * PM-7322 - RegisterUserCommand.cs - refactor ValidateOrgInviteToken logic to always validate the token if we have one. * PM-7322 - RegisterUserCommand.cs - Refactor OrgInviteToken validation to be more clear + validate org invite token even in open registration scenarios + added tests. * PM-7322 - Add more test coverage to RegisterUserWithOptionalOrgInvite * PM-7322 - IRegisterUserCommand - DOCS * PM-7322 - Test RegisterUser * PM-7322 - IRegisterUserCommand - Add more docs. * PM-7322 - Finish updating all existing user service register calls to use the new command. * PM-7322 - RegistrationEmailVerificationTokenable.cs changes + tests * PM-7322 - RegistrationEmailVerificationTokenable.cs changed to only verify email as it's the only thing we need to verify + updated tests. * PM-7322 - Get RegisterUserViaEmailVerificationToken built and tested * PM-7322 - AccountsController.cs - get bones of PostRegisterFinish in place * PM-7322 - SendVerificationEmailForRegistrationCommand - Feature flag timing attack delays per architecture discussion with a default of keeping them around. * PM-7322 - RegisterFinishRequestModel.cs - EmailVerificationToken must be optional for org invite scenarios. * PM-7322 - HandlebarsMailService.cs - SendRegistrationVerificationEmailAsync - must URL encode email to avoid invalid email upon submission to server on complete registration step * PM-7322 - RegisterUserCommandTests.cs - add API key assertions * PM-7322 - Clean up RegisterUserCommand.cs * PM-7322 - Refactor AccountsController.cs existing org invite method and new process to consider new feature flag for delays. * PM-7322 - Add feature flag svc to AccountsControllerTests.cs + add TODO * PM-7322 - AccountsController.cs - Refactor shared IdentityResult logic into private helper. * PM-7322 - Work on getting PostRegisterFinish tests in place. * PM-7322 - AccountsControllerTests.cs - test new method. * PM-7322 - RegisterFinishRequestModel.cs - Update to use required keyword instead of required annotations as it is easier to catch mistakes. * PM-7322 - Fix misspelling * PM-7322 - Integration tests for RegistrationWithEmailVerification * PM-7322 - Fix leaky integration tests. * PM-7322 - Another leaky test fix. * PM-7322 - AccountsControllerTests.cs - fix RegistrationWithEmailVerification_WithOrgInviteToken_Succeeds * PM-7322 - AccountsControllerTests.cs - Finish out integration test suite!
This commit is contained in:
@@ -8,6 +8,7 @@ using Bit.Core.Auth.UserFeatures.Registration;
|
||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||
using Bit.Core.Auth.Utilities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
@@ -21,6 +22,7 @@ using Bit.Core.Utilities;
|
||||
using Bit.Identity.Models.Request.Accounts;
|
||||
using Bit.Identity.Models.Response.Accounts;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Identity.Controllers;
|
||||
@@ -32,35 +34,37 @@ public class AccountsController : Controller
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ILogger<AccountsController> _logger;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IRegisterUserCommand _registerUserCommand;
|
||||
private readonly ICaptchaValidationService _captchaValidationService;
|
||||
private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
|
||||
private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||
private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public AccountsController(
|
||||
ICurrentContext currentContext,
|
||||
ILogger<AccountsController> logger,
|
||||
IUserRepository userRepository,
|
||||
IUserService userService,
|
||||
IRegisterUserCommand registerUserCommand,
|
||||
ICaptchaValidationService captchaValidationService,
|
||||
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
|
||||
IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand,
|
||||
ISendVerificationEmailForRegistrationCommand sendVerificationEmailForRegistrationCommand,
|
||||
IReferenceEventService referenceEventService
|
||||
IReferenceEventService referenceEventService,
|
||||
IFeatureService featureService
|
||||
)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_logger = logger;
|
||||
_userRepository = userRepository;
|
||||
_userService = userService;
|
||||
_registerUserCommand = registerUserCommand;
|
||||
_captchaValidationService = captchaValidationService;
|
||||
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
||||
_getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||
_sendVerificationEmailForRegistrationCommand = sendVerificationEmailForRegistrationCommand;
|
||||
_referenceEventService = referenceEventService;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
@@ -68,21 +72,10 @@ public class AccountsController : Controller
|
||||
public async Task<RegisterResponseModel> PostRegister([FromBody] RegisterRequestModel model)
|
||||
{
|
||||
var user = model.ToUser();
|
||||
var result = await _userService.RegisterUserAsync(user, model.MasterPasswordHash,
|
||||
var identityResult = await _registerUserCommand.RegisterUserWithOptionalOrgInvite(user, model.MasterPasswordHash,
|
||||
model.Token, model.OrganizationUserId);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var captchaBypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user);
|
||||
return new RegisterResponseModel(captchaBypassToken);
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors.Where(e => e.Code != "DuplicateUserName"))
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
|
||||
await Task.Delay(2000);
|
||||
throw new BadRequestException(ModelState);
|
||||
// delaysEnabled false is only for the new registration with email verification process
|
||||
return await ProcessRegistrationResult(identityResult, user, delaysEnabled: true);
|
||||
}
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.EmailVerification)]
|
||||
@@ -109,6 +102,50 @@ public class AccountsController : Controller
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.EmailVerification)]
|
||||
[HttpPost("register/finish")]
|
||||
public async Task<RegisterResponseModel> PostRegisterFinish([FromBody] RegisterFinishRequestModel model)
|
||||
{
|
||||
var user = model.ToUser();
|
||||
|
||||
// Users will either have an org invite token or an email verification token - not both.
|
||||
|
||||
IdentityResult identityResult = null;
|
||||
var delaysEnabled = !_featureService.IsEnabled(FeatureFlagKeys.EmailVerificationDisableTimingDelays);
|
||||
|
||||
if (!string.IsNullOrEmpty(model.OrgInviteToken) && model.OrganizationUserId.HasValue)
|
||||
{
|
||||
identityResult = await _registerUserCommand.RegisterUserWithOptionalOrgInvite(user, model.MasterPasswordHash,
|
||||
model.OrgInviteToken, model.OrganizationUserId);
|
||||
|
||||
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
|
||||
}
|
||||
|
||||
identityResult = await _registerUserCommand.RegisterUserViaEmailVerificationToken(user, model.MasterPasswordHash, model.EmailVerificationToken);
|
||||
|
||||
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
|
||||
}
|
||||
|
||||
private async Task<RegisterResponseModel> ProcessRegistrationResult(IdentityResult result, User user, bool delaysEnabled)
|
||||
{
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var captchaBypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user);
|
||||
return new RegisterResponseModel(captchaBypassToken);
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors.Where(e => e.Code != "DuplicateUserName"))
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
|
||||
if (delaysEnabled)
|
||||
{
|
||||
await Task.Delay(Random.Shared.Next(100, 130));
|
||||
}
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
// Moved from API, If you modify this endpoint, please update API as well. Self hosted installs still use the API endpoints.
|
||||
[HttpPost("prelogin")]
|
||||
public async Task<PreloginResponseModel> PostPrelogin([FromBody] PreloginRequestModel model)
|
||||
|
||||
Reference in New Issue
Block a user