mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42:48 -05:00
Defect/PM-1196 - SSO with Email 2FA Flow - Email Required error fixed (#2874)
* PM-1196 - Created first draft solution for solving SSO with Email 2FA serverside. Per architectural review discussion, will be replacing OTP use with expiring tokenable implementation in order to decouple the OTP implementation from the need for an auth factor when arriving on the email 2FA screen post SSO. * PM-1196 - Refactored OTP solution to leverage newly created SsoEmail2faSessionTokenable. Working now but some code cleanup required. Might revisit whether or not we still send down email alongside the token or not to make the SendEmailLoginAsync method more streamlined. * PM-1196 - Send down email separately on token rejection b/c of 2FA required so that 2FA Controller send email login can be refactored to be much cleaner with email required. * PM-1196 - Fix lint issues w/ dotnet format. * PM-1196 - More formatting issue fixes. * PM-1196 - Remove unnecessary check as email is required again on TwoFactorEmailRequestModel * PM-1196 - Update SsoEmail2faSessionTokenable to expire after just over 2 min to match client side auth service expiration of 2 min with small buffer. * PM-1196 - Fix lint issue w/ dotnet format. * PM-1196 - Per PR feedback, move CustomTokenRequestValidator constructor param to new line * PM-1196 - Per PR feedback, update ThrowDelayedBadRequestExceptionAsync to return a task so that it can be awaited and so that the calling code can handle any exceptions that occur during its execution * PM-1196 - Per PR feedback, refactor SsoEmail2faSessionTokenable to leverage TimeSpan vs double for token expiration lifetime.
This commit is contained in:
@ -5,6 +5,7 @@ using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Utilities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
@ -12,6 +13,7 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Fido2NetLib;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -31,6 +33,7 @@ public class TwoFactorController : Controller
|
||||
private readonly UserManager<User> _userManager;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IVerifyAuthRequestCommand _verifyAuthRequestCommand;
|
||||
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _tokenDataFactory;
|
||||
|
||||
public TwoFactorController(
|
||||
IUserService userService,
|
||||
@ -39,7 +42,8 @@ public class TwoFactorController : Controller
|
||||
GlobalSettings globalSettings,
|
||||
UserManager<User> userManager,
|
||||
ICurrentContext currentContext,
|
||||
IVerifyAuthRequestCommand verifyAuthRequestCommand)
|
||||
IVerifyAuthRequestCommand verifyAuthRequestCommand,
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory)
|
||||
{
|
||||
_userService = userService;
|
||||
_organizationRepository = organizationRepository;
|
||||
@ -48,6 +52,7 @@ public class TwoFactorController : Controller
|
||||
_userManager = userManager;
|
||||
_currentContext = currentContext;
|
||||
_verifyAuthRequestCommand = verifyAuthRequestCommand;
|
||||
_tokenDataFactory = tokenDataFactory;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@ -85,7 +90,8 @@ public class TwoFactorController : Controller
|
||||
}
|
||||
|
||||
[HttpPost("get-authenticator")]
|
||||
public async Task<TwoFactorAuthenticatorResponseModel> GetAuthenticator([FromBody] SecretVerificationRequestModel model)
|
||||
public async Task<TwoFactorAuthenticatorResponseModel> GetAuthenticator(
|
||||
[FromBody] SecretVerificationRequestModel model)
|
||||
{
|
||||
var user = await CheckAsync(model, false);
|
||||
var response = new TwoFactorAuthenticatorResponseModel(user);
|
||||
@ -101,7 +107,7 @@ public class TwoFactorController : Controller
|
||||
model.ToUser(user);
|
||||
|
||||
if (!await _userManager.VerifyTwoFactorTokenAsync(user,
|
||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.Authenticator), model.Token))
|
||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.Authenticator), model.Token))
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
throw new BadRequestException("Token", "Invalid token.");
|
||||
@ -158,7 +164,8 @@ public class TwoFactorController : Controller
|
||||
}
|
||||
catch (DuoException)
|
||||
{
|
||||
throw new BadRequestException("Duo configuration settings are not valid. Please re-check the Duo Admin panel.");
|
||||
throw new BadRequestException(
|
||||
"Duo configuration settings are not valid. Please re-check the Duo Admin panel.");
|
||||
}
|
||||
|
||||
model.ToUser(user);
|
||||
@ -215,7 +222,8 @@ public class TwoFactorController : Controller
|
||||
}
|
||||
catch (DuoException)
|
||||
{
|
||||
throw new BadRequestException("Duo configuration settings are not valid. Please re-check the Duo Admin panel.");
|
||||
throw new BadRequestException(
|
||||
"Duo configuration settings are not valid. Please re-check the Duo Admin panel.");
|
||||
}
|
||||
|
||||
model.ToOrganization(organization);
|
||||
@ -254,12 +262,14 @@ public class TwoFactorController : Controller
|
||||
{
|
||||
throw new BadRequestException("Unable to complete WebAuthn registration.");
|
||||
}
|
||||
|
||||
var response = new TwoFactorWebAuthnResponseModel(user);
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpDelete("webauthn")]
|
||||
public async Task<TwoFactorWebAuthnResponseModel> DeleteWebAuthn([FromBody] TwoFactorWebAuthnDeleteRequestModel model)
|
||||
public async Task<TwoFactorWebAuthnResponseModel> DeleteWebAuthn(
|
||||
[FromBody] TwoFactorWebAuthnDeleteRequestModel model)
|
||||
{
|
||||
var user = await CheckAsync(model, true);
|
||||
await _userService.DeleteWebAuthnKeyAsync(user, model.Id.Value);
|
||||
@ -285,30 +295,46 @@ public class TwoFactorController : Controller
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("send-email-login")]
|
||||
public async Task SendEmailLogin([FromBody] TwoFactorEmailRequestModel model)
|
||||
public async Task SendEmailLoginAsync([FromBody] TwoFactorEmailRequestModel requestModel)
|
||||
{
|
||||
var user = await _userManager.FindByEmailAsync(model.Email.ToLowerInvariant());
|
||||
var user = await _userManager.FindByEmailAsync(requestModel.Email.ToLowerInvariant());
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
// check if 2FA email is from passwordless
|
||||
if (!string.IsNullOrEmpty(model.AuthRequestAccessCode))
|
||||
if (!string.IsNullOrEmpty(requestModel.AuthRequestAccessCode))
|
||||
{
|
||||
if (await _verifyAuthRequestCommand
|
||||
.VerifyAuthRequestAsync(new Guid(model.AuthRequestId), model.AuthRequestAccessCode))
|
||||
.VerifyAuthRequestAsync(new Guid(requestModel.AuthRequestId),
|
||||
requestModel.AuthRequestAccessCode))
|
||||
{
|
||||
await _userService.SendTwoFactorEmailAsync(user);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (await _userService.VerifySecretAsync(user, model.Secret))
|
||||
else if (!string.IsNullOrEmpty(requestModel.SsoEmail2FaSessionToken))
|
||||
{
|
||||
if (this.ValidateSsoEmail2FaToken(requestModel.SsoEmail2FaSessionToken, user))
|
||||
{
|
||||
await _userService.SendTwoFactorEmailAsync(user);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
await this.ThrowDelayedBadRequestExceptionAsync(
|
||||
"Cannot send two-factor email: a valid, non-expired SSO Email 2FA Session token is required to send 2FA emails.",
|
||||
2000);
|
||||
}
|
||||
}
|
||||
else if (await _userService.VerifySecretAsync(user, requestModel.Secret))
|
||||
{
|
||||
await _userService.SendTwoFactorEmailAsync(user);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(2000);
|
||||
throw new BadRequestException("Cannot send two-factor email.");
|
||||
await this.ThrowDelayedBadRequestExceptionAsync(
|
||||
"Cannot send two-factor email.", 2000);
|
||||
}
|
||||
|
||||
[HttpPut("email")]
|
||||
@ -319,7 +345,7 @@ public class TwoFactorController : Controller
|
||||
model.ToUser(user);
|
||||
|
||||
if (!await _userManager.VerifyTwoFactorTokenAsync(user,
|
||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), model.Token))
|
||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), model.Token))
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
throw new BadRequestException("Token", "Invalid token.");
|
||||
@ -377,7 +403,7 @@ public class TwoFactorController : Controller
|
||||
public async Task PostRecover([FromBody] TwoFactorRecoveryRequestModel model)
|
||||
{
|
||||
if (!await _userService.RecoverTwoFactorAsync(model.Email, model.MasterPasswordHash, model.RecoveryCode,
|
||||
_organizationService))
|
||||
_organizationService))
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
throw new BadRequestException(string.Empty, "Invalid information. Try again.");
|
||||
@ -393,7 +419,8 @@ public class TwoFactorController : Controller
|
||||
|
||||
[Obsolete("Leaving this for backwards compatibilty on clients")]
|
||||
[HttpPut("device-verification-settings")]
|
||||
public Task<DeviceVerificationResponseModel> PutDeviceVerificationSettings([FromBody] DeviceVerificationRequestModel model)
|
||||
public Task<DeviceVerificationResponseModel> PutDeviceVerificationSettings(
|
||||
[FromBody] DeviceVerificationRequestModel model)
|
||||
{
|
||||
return Task.FromResult(new DeviceVerificationResponseModel(false, false));
|
||||
}
|
||||
@ -428,7 +455,7 @@ public class TwoFactorController : Controller
|
||||
}
|
||||
|
||||
if (!await _userManager.VerifyTwoFactorTokenAsync(user,
|
||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.YubiKey), value))
|
||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.YubiKey), value))
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
throw new BadRequestException(name, $"{name} is invalid.");
|
||||
@ -438,4 +465,16 @@ public class TwoFactorController : Controller
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ValidateSsoEmail2FaToken(string ssoEmail2FaSessionToken, User user)
|
||||
{
|
||||
return _tokenDataFactory.TryUnprotect(ssoEmail2FaSessionToken, out var decryptedToken) &&
|
||||
decryptedToken.Valid && decryptedToken.TokenIsValid(user);
|
||||
}
|
||||
|
||||
private async Task ThrowDelayedBadRequestExceptionAsync(string message, int delayTime = 2000)
|
||||
{
|
||||
await Task.Delay(delayTime);
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ public class SecretVerificationRequestModel : IValidatableObject
|
||||
{
|
||||
if (string.IsNullOrEmpty(Secret) && string.IsNullOrEmpty(AuthRequestAccessCode))
|
||||
{
|
||||
yield return new ValidationResult("MasterPasswordHash, OTP or AccessCode must be supplied.");
|
||||
yield return new ValidationResult("MasterPasswordHash, OTP, or AccessCode must be supplied.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -202,9 +202,9 @@ public class TwoFactorEmailRequestModel : SecretVerificationRequestModel
|
||||
[EmailAddress]
|
||||
[StringLength(256)]
|
||||
public string Email { get; set; }
|
||||
|
||||
public string AuthRequestId { get; set; }
|
||||
|
||||
// An auth session token used for obtaining email and as an authN factor for the sending of emailed 2FA OTPs.
|
||||
public string SsoEmail2FaSessionToken { get; set; }
|
||||
public User ToUser(User extistingUser)
|
||||
{
|
||||
var providers = extistingUser.GetTwoFactorProviders();
|
||||
@ -225,6 +225,14 @@ public class TwoFactorEmailRequestModel : SecretVerificationRequestModel
|
||||
extistingUser.SetTwoFactorProviders(providers);
|
||||
return extistingUser;
|
||||
}
|
||||
|
||||
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Secret) && string.IsNullOrEmpty(AuthRequestAccessCode) && string.IsNullOrEmpty((SsoEmail2FaSessionToken)))
|
||||
{
|
||||
yield return new ValidationResult("MasterPasswordHash, OTP, AccessCode, or SsoEmail2faSessionToken must be supplied.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class TwoFactorWebAuthnRequestModel : TwoFactorWebAuthnDeleteRequestModel
|
||||
|
Reference in New Issue
Block a user