mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 16:12:49 -05:00
[PM-1815] Include Member Decryption Type in Token Response (#2927)
* Include Member Decryption Type * Make ICurrentContext protected from base class * Return MemberDecryptionType * Extend WebApplicationFactoryBase - Allow for service subsitution * Create SSO Tests - Mock IAuthorizationCodeStore so the SSO process can be limited to Identity * Add MemberDecryptionOptions * Remove Unused Property Assertion * Make MemberDecryptionOptions an Array * Address PR Feedback * Make HasAdminApproval Policy Aware * Format * Use Object Instead * Add UserDecryptionOptions File
This commit is contained in:
50
src/Core/Auth/Models/Api/Response/UserDecryptionOptions.cs
Normal file
50
src/Core/Auth/Models/Api/Response/UserDecryptionOptions.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Auth.Models.Api.Response;
|
||||
|
||||
public class UserDecryptionOptions : ResponseModel
|
||||
{
|
||||
public UserDecryptionOptions() : base("userDecryptionOptions")
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool HasMasterPassword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public TrustedDeviceUserDecryptionOption? TrustedDeviceOption { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public KeyConnectorUserDecryptionOption? KeyConnectorOption { get; set; }
|
||||
}
|
||||
|
||||
public class TrustedDeviceUserDecryptionOption
|
||||
{
|
||||
public bool HasAdminApproval { get; }
|
||||
|
||||
public TrustedDeviceUserDecryptionOption(bool hasAdminApproval)
|
||||
{
|
||||
HasAdminApproval = hasAdminApproval;
|
||||
}
|
||||
}
|
||||
|
||||
public class KeyConnectorUserDecryptionOption
|
||||
{
|
||||
public string KeyConnectorUrl { get; }
|
||||
|
||||
public KeyConnectorUserDecryptionOption(string keyConnectorUrl)
|
||||
{
|
||||
KeyConnectorUrl = keyConnectorUrl;
|
||||
}
|
||||
}
|
@ -37,12 +37,13 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _tokenDataFactory;
|
||||
|
||||
protected ICurrentContext CurrentContext { get; }
|
||||
protected IPolicyService PolicyService { get; }
|
||||
|
||||
public BaseRequestValidator(
|
||||
UserManager<User> userManager,
|
||||
IDeviceRepository deviceRepository,
|
||||
@ -73,11 +74,10 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_mailService = mailService;
|
||||
_logger = logger;
|
||||
_currentContext = currentContext;
|
||||
CurrentContext = currentContext;
|
||||
_globalSettings = globalSettings;
|
||||
_policyService = policyService;
|
||||
PolicyService = policyService;
|
||||
_userRepository = userRepository;
|
||||
_policyService = policyService;
|
||||
_tokenDataFactory = tokenDataFactory;
|
||||
}
|
||||
|
||||
@ -284,7 +284,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
{
|
||||
_logger.LogWarning(Constants.BypassFiltersEventId,
|
||||
string.Format("Failed login attempt{0}{1}", twoFactorRequest ? ", 2FA invalid." : ".",
|
||||
$" {_currentContext.IpAddress}"));
|
||||
$" {CurrentContext.IpAddress}"));
|
||||
}
|
||||
|
||||
await Task.Delay(2000); // Delay for brute force.
|
||||
@ -314,7 +314,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
(await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0;
|
||||
|
||||
Organization firstEnabledOrg = null;
|
||||
var orgs = (await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id))
|
||||
var orgs = (await CurrentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id))
|
||||
.ToList();
|
||||
if (orgs.Any())
|
||||
{
|
||||
@ -341,7 +341,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
}
|
||||
|
||||
// Check if user belongs to any organization with an active SSO policy
|
||||
var anySsoPoliciesApplicableToUser = await _policyService.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
|
||||
var anySsoPoliciesApplicableToUser = await PolicyService.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
|
||||
if (anySsoPoliciesApplicableToUser)
|
||||
{
|
||||
return false;
|
||||
@ -501,7 +501,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
if (!_globalSettings.DisableEmailNewDevice)
|
||||
{
|
||||
await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now,
|
||||
_currentContext.IpAddress);
|
||||
CurrentContext.IpAddress);
|
||||
}
|
||||
}
|
||||
|
||||
@ -543,11 +543,11 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
{
|
||||
if (twoFactorInvalid)
|
||||
{
|
||||
await _mailService.SendFailedTwoFactorAttemptsEmailAsync(user.Email, utcNow, _currentContext.IpAddress);
|
||||
await _mailService.SendFailedTwoFactorAttemptsEmailAsync(user.Email, utcNow, CurrentContext.IpAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _mailService.SendFailedLoginAttemptsEmailAsync(user.Email, utcNow, _currentContext.IpAddress);
|
||||
await _mailService.SendFailedLoginAttemptsEmailAsync(user.Email, utcNow, CurrentContext.IpAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -562,7 +562,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
private async Task<MasterPasswordPolicyResponseModel> GetMasterPasswordPolicy(User user)
|
||||
{
|
||||
// Check current context/cache to see if user is in any organizations, avoids extra DB call if not
|
||||
var orgs = (await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id))
|
||||
var orgs = (await CurrentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id))
|
||||
.ToList();
|
||||
|
||||
if (!orgs.Any())
|
||||
@ -570,6 +570,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
return null;
|
||||
}
|
||||
|
||||
return new MasterPasswordPolicyResponseModel(await _policyService.GetMasterPasswordPolicyForUserAsync(user));
|
||||
return new MasterPasswordPolicyResponseModel(await PolicyService.GetMasterPasswordPolicyForUserAsync(user));
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Models.Api.Response;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -15,13 +19,16 @@ using IdentityServer4.Extensions;
|
||||
using IdentityServer4.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenRequestValidationContext>,
|
||||
ICustomTokenRequestValidator
|
||||
{
|
||||
private UserManager<User> _userManager;
|
||||
private readonly UserManager<User> _userManager;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public CustomTokenRequestValidator(
|
||||
UserManager<User> userManager,
|
||||
@ -41,7 +48,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserRepository userRepository,
|
||||
IPolicyService policyService,
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory)
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
||||
IFeatureService featureService)
|
||||
: base(userManager, deviceRepository, deviceService, userService, eventService,
|
||||
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
|
||||
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository,
|
||||
@ -49,6 +57,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
{
|
||||
_userManager = userManager;
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
public async Task ValidateAsync(CustomTokenRequestValidationContext context)
|
||||
@ -101,6 +110,20 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
}
|
||||
}
|
||||
|
||||
// Attempts to find ssoConfigData for a given validate request subject
|
||||
// this is actually guarenteed to pretty often be null, because more than just sso login requests will come
|
||||
// through here
|
||||
var ssoConfigData = await GetSsoConfigurationDataAsync(context.Result.ValidatedRequest.Subject);
|
||||
|
||||
// You can't put this below the user.MasterPassword != null check because TDE users can still have a MasterPassword
|
||||
// It's worth noting that CurrentContext here will build a user in LaunchDarkly that is anonymous but DOES belong
|
||||
// to an organization. So we will not be able to turn this feature on for only a single user, only for an entire
|
||||
// organization at a time.
|
||||
if (ssoConfigData != null && _featureService.IsEnabled(FeatureFlagKeys.TrustedDeviceEncryption, CurrentContext))
|
||||
{
|
||||
context.Result.CustomResponse["UserDecryptionOptions"] = await CreateUserDecryptionOptionsAsync(ssoConfigData, user);
|
||||
}
|
||||
|
||||
if (context.Result.CustomResponse == null || user.MasterPassword != null)
|
||||
{
|
||||
return;
|
||||
@ -122,23 +145,34 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
}
|
||||
|
||||
// SSO login
|
||||
var organizationClaim = context.Result.ValidatedRequest.Subject?.FindFirst(c => c.Type == "organizationId");
|
||||
if (organizationClaim?.Value != null)
|
||||
// This does a double check, that ssoConfigData is not null and that it has the KeyConnector member decryption type
|
||||
if (ssoConfigData is { MemberDecryptionType: MemberDecryptionType.KeyConnector } && !string.IsNullOrEmpty(ssoConfigData.KeyConnectorUrl))
|
||||
{
|
||||
var organizationId = new Guid(organizationClaim.Value);
|
||||
|
||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organizationId);
|
||||
var ssoConfigData = ssoConfig.GetData();
|
||||
|
||||
if (ssoConfigData is { MemberDecryptionType: MemberDecryptionType.KeyConnector } && !string.IsNullOrEmpty(ssoConfigData.KeyConnectorUrl))
|
||||
{
|
||||
context.Result.CustomResponse["KeyConnectorUrl"] = ssoConfigData.KeyConnectorUrl;
|
||||
// Prevent clients redirecting to set-password
|
||||
context.Result.CustomResponse["ResetMasterPassword"] = false;
|
||||
}
|
||||
// TODO: Can be removed in the future
|
||||
context.Result.CustomResponse["KeyConnectorUrl"] = ssoConfigData.KeyConnectorUrl;
|
||||
// Prevent clients redirecting to set-password
|
||||
context.Result.CustomResponse["ResetMasterPassword"] = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<SsoConfigurationData?> GetSsoConfigurationDataAsync(ClaimsPrincipal? subject)
|
||||
{
|
||||
var organizationClaim = subject?.FindFirstValue("organizationId");
|
||||
|
||||
if (organizationClaim == null || !Guid.TryParse(organizationClaim, out var organizationId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organizationId);
|
||||
if (ssoConfig == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ssoConfig.GetData();
|
||||
}
|
||||
|
||||
protected override void SetTwoFactorResult(CustomTokenRequestValidationContext context,
|
||||
Dictionary<string, object> customResponse)
|
||||
{
|
||||
@ -164,4 +198,29 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
context.Result.IsError = true;
|
||||
context.Result.CustomResponse = customResponse;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to create a list of all possible ways the newly authenticated user can decrypt their vault contents
|
||||
/// </summary>
|
||||
private async Task<UserDecryptionOptions> CreateUserDecryptionOptionsAsync(SsoConfigurationData ssoConfigurationData, User user)
|
||||
{
|
||||
var userDecryptionOption = new UserDecryptionOptions();
|
||||
if (ssoConfigurationData is { MemberDecryptionType: MemberDecryptionType.KeyConnector } && !string.IsNullOrEmpty(ssoConfigurationData.KeyConnectorUrl))
|
||||
{
|
||||
// KeyConnector makes it mutually exclusive
|
||||
userDecryptionOption.KeyConnectorOption = new KeyConnectorUserDecryptionOption(ssoConfigurationData.KeyConnectorUrl);
|
||||
return userDecryptionOption;
|
||||
}
|
||||
|
||||
if (ssoConfigurationData is { MemberDecryptionType: MemberDecryptionType.TrustedDeviceEncryption })
|
||||
{
|
||||
var hasAdminApproval = await PolicyService.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.ResetPassword);
|
||||
// TrustedDeviceEncryption only exists for SSO, but if that ever changes this value won't always be true
|
||||
userDecryptionOption.TrustedDeviceOption = new TrustedDeviceUserDecryptionOption(hasAdminApproval);
|
||||
}
|
||||
|
||||
userDecryptionOption.HasMasterPassword = !string.IsNullOrEmpty(user.MasterPassword);
|
||||
|
||||
return userDecryptionOption;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user