1
0
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:
Jared Snider
2024-07-02 17:03:36 -04:00
committed by GitHub
parent da4f436a71
commit 8471326b1e
20 changed files with 1239 additions and 187 deletions

View File

@@ -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)