1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-22 12:15:36 +01: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:
Jared Snider 2023-05-04 15:12:03 -04:00 committed by GitHub
parent b87846f97f
commit 2ac513e15a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 181 additions and 56 deletions

View File

@ -5,6 +5,7 @@ using Bit.Api.Models.Request;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces; using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Utilities; using Bit.Core.Auth.Utilities;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
@ -12,6 +13,7 @@ using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Fido2NetLib; using Fido2NetLib;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -31,6 +33,7 @@ public class TwoFactorController : Controller
private readonly UserManager<User> _userManager; private readonly UserManager<User> _userManager;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IVerifyAuthRequestCommand _verifyAuthRequestCommand; private readonly IVerifyAuthRequestCommand _verifyAuthRequestCommand;
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _tokenDataFactory;
public TwoFactorController( public TwoFactorController(
IUserService userService, IUserService userService,
@ -39,7 +42,8 @@ public class TwoFactorController : Controller
GlobalSettings globalSettings, GlobalSettings globalSettings,
UserManager<User> userManager, UserManager<User> userManager,
ICurrentContext currentContext, ICurrentContext currentContext,
IVerifyAuthRequestCommand verifyAuthRequestCommand) IVerifyAuthRequestCommand verifyAuthRequestCommand,
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory)
{ {
_userService = userService; _userService = userService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
@ -48,6 +52,7 @@ public class TwoFactorController : Controller
_userManager = userManager; _userManager = userManager;
_currentContext = currentContext; _currentContext = currentContext;
_verifyAuthRequestCommand = verifyAuthRequestCommand; _verifyAuthRequestCommand = verifyAuthRequestCommand;
_tokenDataFactory = tokenDataFactory;
} }
[HttpGet("")] [HttpGet("")]
@ -85,7 +90,8 @@ public class TwoFactorController : Controller
} }
[HttpPost("get-authenticator")] [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 user = await CheckAsync(model, false);
var response = new TwoFactorAuthenticatorResponseModel(user); var response = new TwoFactorAuthenticatorResponseModel(user);
@ -101,7 +107,7 @@ public class TwoFactorController : Controller
model.ToUser(user); model.ToUser(user);
if (!await _userManager.VerifyTwoFactorTokenAsync(user, if (!await _userManager.VerifyTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(TwoFactorProviderType.Authenticator), model.Token)) CoreHelpers.CustomProviderName(TwoFactorProviderType.Authenticator), model.Token))
{ {
await Task.Delay(2000); await Task.Delay(2000);
throw new BadRequestException("Token", "Invalid token."); throw new BadRequestException("Token", "Invalid token.");
@ -158,7 +164,8 @@ public class TwoFactorController : Controller
} }
catch (DuoException) 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); model.ToUser(user);
@ -215,7 +222,8 @@ public class TwoFactorController : Controller
} }
catch (DuoException) 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); model.ToOrganization(organization);
@ -254,12 +262,14 @@ public class TwoFactorController : Controller
{ {
throw new BadRequestException("Unable to complete WebAuthn registration."); throw new BadRequestException("Unable to complete WebAuthn registration.");
} }
var response = new TwoFactorWebAuthnResponseModel(user); var response = new TwoFactorWebAuthnResponseModel(user);
return response; return response;
} }
[HttpDelete("webauthn")] [HttpDelete("webauthn")]
public async Task<TwoFactorWebAuthnResponseModel> DeleteWebAuthn([FromBody] TwoFactorWebAuthnDeleteRequestModel model) public async Task<TwoFactorWebAuthnResponseModel> DeleteWebAuthn(
[FromBody] TwoFactorWebAuthnDeleteRequestModel model)
{ {
var user = await CheckAsync(model, true); var user = await CheckAsync(model, true);
await _userService.DeleteWebAuthnKeyAsync(user, model.Id.Value); await _userService.DeleteWebAuthnKeyAsync(user, model.Id.Value);
@ -285,30 +295,46 @@ public class TwoFactorController : Controller
[AllowAnonymous] [AllowAnonymous]
[HttpPost("send-email-login")] [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) if (user != null)
{ {
// check if 2FA email is from passwordless // check if 2FA email is from passwordless
if (!string.IsNullOrEmpty(model.AuthRequestAccessCode)) if (!string.IsNullOrEmpty(requestModel.AuthRequestAccessCode))
{ {
if (await _verifyAuthRequestCommand if (await _verifyAuthRequestCommand
.VerifyAuthRequestAsync(new Guid(model.AuthRequestId), model.AuthRequestAccessCode)) .VerifyAuthRequestAsync(new Guid(requestModel.AuthRequestId),
requestModel.AuthRequestAccessCode))
{ {
await _userService.SendTwoFactorEmailAsync(user); await _userService.SendTwoFactorEmailAsync(user);
return; 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); await _userService.SendTwoFactorEmailAsync(user);
return; return;
} }
} }
await Task.Delay(2000); await this.ThrowDelayedBadRequestExceptionAsync(
throw new BadRequestException("Cannot send two-factor email."); "Cannot send two-factor email.", 2000);
} }
[HttpPut("email")] [HttpPut("email")]
@ -319,7 +345,7 @@ public class TwoFactorController : Controller
model.ToUser(user); model.ToUser(user);
if (!await _userManager.VerifyTwoFactorTokenAsync(user, if (!await _userManager.VerifyTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), model.Token)) CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), model.Token))
{ {
await Task.Delay(2000); await Task.Delay(2000);
throw new BadRequestException("Token", "Invalid token."); throw new BadRequestException("Token", "Invalid token.");
@ -377,7 +403,7 @@ public class TwoFactorController : Controller
public async Task PostRecover([FromBody] TwoFactorRecoveryRequestModel model) public async Task PostRecover([FromBody] TwoFactorRecoveryRequestModel model)
{ {
if (!await _userService.RecoverTwoFactorAsync(model.Email, model.MasterPasswordHash, model.RecoveryCode, if (!await _userService.RecoverTwoFactorAsync(model.Email, model.MasterPasswordHash, model.RecoveryCode,
_organizationService)) _organizationService))
{ {
await Task.Delay(2000); await Task.Delay(2000);
throw new BadRequestException(string.Empty, "Invalid information. Try again."); 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")] [Obsolete("Leaving this for backwards compatibilty on clients")]
[HttpPut("device-verification-settings")] [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)); return Task.FromResult(new DeviceVerificationResponseModel(false, false));
} }
@ -428,7 +455,7 @@ public class TwoFactorController : Controller
} }
if (!await _userManager.VerifyTwoFactorTokenAsync(user, if (!await _userManager.VerifyTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(TwoFactorProviderType.YubiKey), value)) CoreHelpers.CustomProviderName(TwoFactorProviderType.YubiKey), value))
{ {
await Task.Delay(2000); await Task.Delay(2000);
throw new BadRequestException(name, $"{name} is invalid."); throw new BadRequestException(name, $"{name} is invalid.");
@ -438,4 +465,16 @@ public class TwoFactorController : Controller
await Task.Delay(500); 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);
}
} }

View File

@ -14,7 +14,7 @@ public class SecretVerificationRequestModel : IValidatableObject
{ {
if (string.IsNullOrEmpty(Secret) && string.IsNullOrEmpty(AuthRequestAccessCode)) 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.");
} }
} }
} }

View File

@ -202,9 +202,9 @@ public class TwoFactorEmailRequestModel : SecretVerificationRequestModel
[EmailAddress] [EmailAddress]
[StringLength(256)] [StringLength(256)]
public string Email { get; set; } public string Email { get; set; }
public string AuthRequestId { 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) public User ToUser(User extistingUser)
{ {
var providers = extistingUser.GetTwoFactorProviders(); var providers = extistingUser.GetTwoFactorProviders();
@ -225,6 +225,14 @@ public class TwoFactorEmailRequestModel : SecretVerificationRequestModel
extistingUser.SetTwoFactorProviders(providers); extistingUser.SetTwoFactorProviders(providers);
return extistingUser; 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 public class TwoFactorWebAuthnRequestModel : TwoFactorWebAuthnDeleteRequestModel

View File

@ -0,0 +1,50 @@
using System.Text.Json.Serialization;
using Bit.Core.Entities;
using Bit.Core.Tokens;
namespace Bit.Core.Auth.Models.Business.Tokenables;
// This token just provides a verifiable authN mechanism for the API service
// TwoFactorController.cs SendEmailLogin anonymous endpoint so it cannot be
// used maliciously.
public class SsoEmail2faSessionTokenable : ExpiringTokenable
{
// Just over 2 min expiration (client expires session after 2 min)
private static readonly TimeSpan _tokenLifetime = TimeSpan.FromMinutes(2.05);
public const string ClearTextPrefix = "BwSsoEmail2FaSessionToken_";
public const string DataProtectorPurpose = "SsoEmail2faSessionTokenDataProtector";
public const string TokenIdentifier = "SsoEmail2faSessionToken";
public string Identifier { get; set; } = TokenIdentifier;
public Guid Id { get; set; }
public string Email { get; set; }
[JsonConstructor]
public SsoEmail2faSessionTokenable()
{
ExpirationDate = DateTime.UtcNow.Add(_tokenLifetime);
}
public SsoEmail2faSessionTokenable(User user) : this()
{
Id = user?.Id ?? default;
Email = user?.Email;
}
public bool TokenIsValid(User user)
{
if (Id == default || Email == default || user == null)
{
return false;
}
return Id == user.Id &&
Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase);
}
// Validates deserialized
protected override bool TokenIsValid() =>
Identifier == TokenIdentifier && Id != default && !string.IsNullOrWhiteSpace(Email);
}

View File

@ -6,6 +6,7 @@ using Bit.Core;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity; using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models; using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -16,6 +17,7 @@ using Bit.Core.Models.Data.Organizations;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using IdentityServer4.Validation; using IdentityServer4.Validation;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
@ -40,6 +42,7 @@ public abstract class BaseRequestValidator<T> where T : class
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IPolicyService _policyService; private readonly IPolicyService _policyService;
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _tokenDataFactory;
public BaseRequestValidator( public BaseRequestValidator(
UserManager<User> userManager, UserManager<User> userManager,
@ -57,7 +60,8 @@ public abstract class BaseRequestValidator<T> where T : class
GlobalSettings globalSettings, GlobalSettings globalSettings,
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
IUserRepository userRepository, IUserRepository userRepository,
IPolicyService policyService) IPolicyService policyService,
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory)
{ {
_userManager = userManager; _userManager = userManager;
_deviceRepository = deviceRepository; _deviceRepository = deviceRepository;
@ -75,6 +79,7 @@ public abstract class BaseRequestValidator<T> where T : class
_policyRepository = policyRepository; _policyRepository = policyRepository;
_userRepository = userRepository; _userRepository = userRepository;
_policyService = policyService; _policyService = policyService;
_tokenDataFactory = tokenDataFactory;
} }
protected async Task ValidateAsync(T context, ValidatedTokenRequest request, protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
@ -92,7 +97,7 @@ public abstract class BaseRequestValidator<T> where T : class
var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString(); var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString();
var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1"; var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1";
var twoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) && var twoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) &&
!string.IsNullOrWhiteSpace(twoFactorProvider); !string.IsNullOrWhiteSpace(twoFactorProvider);
var valid = await ValidateContextAsync(context, validatorContext); var valid = await ValidateContextAsync(context, validatorContext);
var user = validatorContext.User; var user = validatorContext.User;
@ -100,6 +105,7 @@ public abstract class BaseRequestValidator<T> where T : class
{ {
await UpdateFailedAuthDetailsAsync(user, false, !validatorContext.KnownDevice); await UpdateFailedAuthDetailsAsync(user, false, !validatorContext.KnownDevice);
} }
if (!valid || isBot) if (!valid || isBot)
{ {
await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user); await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user);
@ -150,14 +156,16 @@ public abstract class BaseRequestValidator<T> where T : class
await BuildErrorResultAsync("No device information provided.", false, context, user); await BuildErrorResultAsync("No device information provided.", false, context, user);
return; return;
} }
await BuildSuccessResultAsync(user, context, device, twoFactorRequest && twoFactorRemember); await BuildSuccessResultAsync(user, context, device, twoFactorRequest && twoFactorRemember);
} }
else else
{ {
SetSsoResult(context, new Dictionary<string, object> SetSsoResult(context,
{{ new Dictionary<string, object>
"ErrorModel", new ErrorResponseModel("SSO authentication is required.") {
}}); { "ErrorModel", new ErrorResponseModel("SSO authentication is required.") }
});
} }
} }
@ -240,13 +248,23 @@ public abstract class BaseRequestValidator<T> where T : class
providers.Add(((byte)provider.Key).ToString(), infoDict); providers.Add(((byte)provider.Key).ToString(), infoDict);
} }
SetTwoFactorResult(context, var twoFactorResultDict = new Dictionary<string, object>
new Dictionary<string, object> {
{ { "TwoFactorProviders", providers.Keys },
{ "TwoFactorProviders", providers.Keys }, { "TwoFactorProviders2", providers },
{ "TwoFactorProviders2", providers }, { "MasterPasswordPolicy", await GetMasterPasswordPolicy(user) },
{ "MasterPasswordPolicy", await GetMasterPasswordPolicy(user) } };
});
// If we have email as a 2FA provider, we might need an SsoEmail2fa Session Token
if (enabledProviders.Any(p => p.Key == TwoFactorProviderType.Email))
{
twoFactorResultDict.Add("SsoEmail2faSessionToken",
_tokenDataFactory.Protect(new SsoEmail2faSessionTokenable(user)));
twoFactorResultDict.Add("Email", user.Email);
}
SetTwoFactorResult(context, twoFactorResultDict);
if (enabledProviders.Count() == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email) if (enabledProviders.Count() == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email)
{ {
@ -272,10 +290,7 @@ public abstract class BaseRequestValidator<T> where T : class
await Task.Delay(2000); // Delay for brute force. await Task.Delay(2000); // Delay for brute force.
SetErrorResult(context, SetErrorResult(context,
new Dictionary<string, object> new Dictionary<string, object> { { "ErrorModel", new ErrorResponseModel(message) } });
{{
"ErrorModel", new ErrorResponseModel(message)
}});
} }
protected abstract void SetTwoFactorResult(T context, Dictionary<string, object> customResponse); protected abstract void SetTwoFactorResult(T context, Dictionary<string, object> customResponse);
@ -296,8 +311,8 @@ public abstract class BaseRequestValidator<T> where T : class
} }
var individualRequired = _userManager.SupportsUserTwoFactor && var individualRequired = _userManager.SupportsUserTwoFactor &&
await _userManager.GetTwoFactorEnabledAsync(user) && await _userManager.GetTwoFactorEnabledAsync(user) &&
(await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0; (await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0;
Organization firstEnabledOrg = null; Organization firstEnabledOrg = null;
var orgs = (await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)) var orgs = (await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id))
@ -346,7 +361,8 @@ public abstract class BaseRequestValidator<T> where T : class
PolicyType.RequireSso); PolicyType.RequireSso);
// Owners and Admins are exempt from this policy // Owners and Admins are exempt from this policy
if (orgPolicy != null && orgPolicy.Enabled && if (orgPolicy != null && orgPolicy.Enabled &&
(_globalSettings.Sso.EnforceSsoPolicyForAllUsers || (userOrg.Type != OrganizationUserType.Owner && userOrg.Type != OrganizationUserType.Admin))) (_globalSettings.Sso.EnforceSsoPolicyForAllUsers ||
(userOrg.Type != OrganizationUserType.Owner && userOrg.Type != OrganizationUserType.Admin)))
{ {
return false; return false;
} }
@ -361,7 +377,7 @@ public abstract class BaseRequestValidator<T> where T : class
private bool OrgUsing2fa(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId) private bool OrgUsing2fa(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)
{ {
return orgAbilities != null && orgAbilities.ContainsKey(orgId) && return orgAbilities != null && orgAbilities.ContainsKey(orgId) &&
orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa; orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa;
} }
private bool OrgCanUseSso(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId) private bool OrgCanUseSso(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)
@ -408,6 +424,7 @@ public abstract class BaseRequestValidator<T> where T : class
{ {
return false; return false;
} }
return await _userManager.VerifyTwoFactorTokenAsync(user, return await _userManager.VerifyTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(type), token); CoreHelpers.CustomProviderName(type), token);
case TwoFactorProviderType.OrganizationDuo: case TwoFactorProviderType.OrganizationDuo:
@ -457,18 +474,13 @@ public abstract class BaseRequestValidator<T> where T : class
} }
else if (type == TwoFactorProviderType.Email) else if (type == TwoFactorProviderType.Email)
{ {
return new Dictionary<string, object> return new Dictionary<string, object> { ["Email"] = token };
{
["Email"] = token
};
} }
else if (type == TwoFactorProviderType.YubiKey) else if (type == TwoFactorProviderType.YubiKey)
{ {
return new Dictionary<string, object> return new Dictionary<string, object> { ["Nfc"] = (bool)provider.MetaData["Nfc"] };
{
["Nfc"] = (bool)provider.MetaData["Nfc"]
};
} }
return null; return null;
case TwoFactorProviderType.OrganizationDuo: case TwoFactorProviderType.OrganizationDuo:
if (await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization)) if (await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization))
@ -479,6 +491,7 @@ public abstract class BaseRequestValidator<T> where T : class
["Signature"] = await _organizationDuoWebTokenProvider.GenerateAsync(organization, user) ["Signature"] = await _organizationDuoWebTokenProvider.GenerateAsync(organization, user)
}; };
} }
return null; return null;
default: default:
return null; return null;

View File

@ -1,5 +1,6 @@
using System.Security.Claims; using System.Security.Claims;
using Bit.Core.Auth.Identity; using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
@ -7,6 +8,7 @@ using Bit.Core.IdentityServer;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tokens;
using IdentityModel; using IdentityModel;
using IdentityServer4.Extensions; using IdentityServer4.Extensions;
using IdentityServer4.Validation; using IdentityServer4.Validation;
@ -37,11 +39,12 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
ISsoConfigRepository ssoConfigRepository, ISsoConfigRepository ssoConfigRepository,
IUserRepository userRepository, IUserRepository userRepository,
IPolicyService policyService) IPolicyService policyService,
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory)
: base(userManager, deviceRepository, deviceService, userService, eventService, : base(userManager, deviceRepository, deviceService, userService, eventService,
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository, organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository, applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository,
userRepository, policyService) userRepository, policyService, tokenDataFactory)
{ {
_userManager = userManager; _userManager = userManager;
_ssoConfigRepository = ssoConfigRepository; _ssoConfigRepository = ssoConfigRepository;
@ -73,11 +76,13 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
CustomValidatorRequestContext validatorContext) CustomValidatorRequestContext validatorContext)
{ {
var email = context.Result.ValidatedRequest.Subject?.GetDisplayName() var email = context.Result.ValidatedRequest.Subject?.GetDisplayName()
?? context.Result.ValidatedRequest.ClientClaims?.FirstOrDefault(claim => claim.Type == JwtClaimTypes.Email)?.Value; ?? context.Result.ValidatedRequest.ClientClaims
?.FirstOrDefault(claim => claim.Type == JwtClaimTypes.Email)?.Value;
if (!string.IsNullOrWhiteSpace(email)) if (!string.IsNullOrWhiteSpace(email))
{ {
validatorContext.User = await _userManager.FindByEmailAsync(email); validatorContext.User = await _userManager.FindByEmailAsync(email);
} }
return validatorContext.User != null; return validatorContext.User != null;
} }
@ -111,6 +116,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
context.Result.CustomResponse["ApiUseKeyConnector"] = true; context.Result.CustomResponse["ApiUseKeyConnector"] = true;
context.Result.CustomResponse["ResetMasterPassword"] = false; context.Result.CustomResponse["ResetMasterPassword"] = false;
} }
return; return;
} }

View File

@ -1,11 +1,13 @@
using System.Security.Claims; using System.Security.Claims;
using Bit.Core.Auth.Identity; using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Services; using Bit.Core.Auth.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using IdentityServer4.Models; using IdentityServer4.Models;
using IdentityServer4.Validation; using IdentityServer4.Validation;
@ -39,11 +41,12 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
ICaptchaValidationService captchaValidationService, ICaptchaValidationService captchaValidationService,
IAuthRequestRepository authRequestRepository, IAuthRequestRepository authRequestRepository,
IUserRepository userRepository, IUserRepository userRepository,
IPolicyService policyService) IPolicyService policyService,
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory)
: base(userManager, deviceRepository, deviceService, userService, eventService, : base(userManager, deviceRepository, deviceService, userService, eventService,
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository, organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository, applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository,
userRepository, policyService) userRepository, policyService, tokenDataFactory)
{ {
_userManager = userManager; _userManager = userManager;
_userService = userService; _userService = userService;

View File

@ -157,6 +157,12 @@ public static class ServiceCollectionExtensions
SsoTokenable.DataProtectorPurpose, SsoTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider(), serviceProvider.GetDataProtectionProvider(),
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<SsoTokenable>>>())); serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<SsoTokenable>>>()));
services.AddSingleton<IDataProtectorTokenFactory<SsoEmail2faSessionTokenable>>(serviceProvider =>
new DataProtectorTokenFactory<SsoEmail2faSessionTokenable>(
SsoEmail2faSessionTokenable.ClearTextPrefix,
SsoEmail2faSessionTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider(),
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<SsoEmail2faSessionTokenable>>>()));
} }
public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings) public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings)