mirror of
https://github.com/bitwarden/server
synced 2026-01-29 15:53:36 +00:00
* Initial refactor
* Add WebauthnPRFOptions to syncResponse
* MAYBE: Use KM owned ResponseModel?
* REVERT ^- Keep using PrfUnlockOptions for simplicity
This reverts commit 5a34e7dfa8.
* UserDecryptionOptions: Only send one credential
* format
* Update UserDecryptionOptions.cs
* format
* Added feature flag (#6600)
209 lines
8.2 KiB
C#
209 lines
8.2 KiB
C#
using Bit.Core.Auth.Entities;
|
|
using Bit.Core.Auth.Enums;
|
|
using Bit.Core.Auth.Models.Api.Response;
|
|
using Bit.Core.Auth.Utilities;
|
|
using Bit.Core.Context;
|
|
using Bit.Core.Entities;
|
|
using Bit.Core.Enums;
|
|
using Bit.Core.KeyManagement.Models.Api.Response;
|
|
using Bit.Core.Repositories;
|
|
using Bit.Core.Utilities;
|
|
using Bit.Identity.Utilities;
|
|
|
|
namespace Bit.Identity.IdentityServer;
|
|
|
|
#nullable enable
|
|
/// <summary>
|
|
/// Used to create a list of all possible ways the newly authenticated user can decrypt their vault contents
|
|
///
|
|
/// Note: Do not use this as an injected service if you intend to build multiple independent UserDecryptionOptions
|
|
/// </summary>
|
|
public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
|
|
{
|
|
private readonly ICurrentContext _currentContext;
|
|
private readonly IDeviceRepository _deviceRepository;
|
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
|
private readonly ILoginApprovingClientTypes _loginApprovingClientTypes;
|
|
private UserDecryptionOptions _options = new UserDecryptionOptions();
|
|
private User _user = null!;
|
|
private SsoConfig? _ssoConfig;
|
|
private Device? _device;
|
|
|
|
public UserDecryptionOptionsBuilder(
|
|
ICurrentContext currentContext,
|
|
IDeviceRepository deviceRepository,
|
|
IOrganizationUserRepository organizationUserRepository,
|
|
ILoginApprovingClientTypes loginApprovingClientTypes
|
|
)
|
|
{
|
|
_currentContext = currentContext;
|
|
_deviceRepository = deviceRepository;
|
|
_organizationUserRepository = organizationUserRepository;
|
|
_loginApprovingClientTypes = loginApprovingClientTypes;
|
|
}
|
|
|
|
public IUserDecryptionOptionsBuilder ForUser(User user)
|
|
{
|
|
_user = user;
|
|
return this;
|
|
}
|
|
|
|
public IUserDecryptionOptionsBuilder WithSso(SsoConfig ssoConfig)
|
|
{
|
|
_ssoConfig = ssoConfig;
|
|
return this;
|
|
}
|
|
|
|
public IUserDecryptionOptionsBuilder WithDevice(Device device)
|
|
{
|
|
_device = device;
|
|
return this;
|
|
}
|
|
|
|
public IUserDecryptionOptionsBuilder WithWebAuthnLoginCredential(WebAuthnCredential credential)
|
|
{
|
|
if (credential.GetPrfStatus() == WebAuthnPrfStatus.Enabled)
|
|
{
|
|
_options.WebAuthnPrfOption = new WebAuthnPrfDecryptionOption(
|
|
credential.EncryptedPrivateKey,
|
|
credential.EncryptedUserKey,
|
|
credential.CredentialId,
|
|
[] // Stored credentials currently lack Transports, just send an empty array for now
|
|
);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
public async Task<UserDecryptionOptions> BuildAsync()
|
|
{
|
|
BuildMasterPasswordUnlock();
|
|
BuildKeyConnectorOptions();
|
|
await BuildTrustedDeviceOptionsAsync();
|
|
|
|
return _options;
|
|
}
|
|
|
|
private void BuildKeyConnectorOptions()
|
|
{
|
|
if (_ssoConfig == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var ssoConfigurationData = _ssoConfig.GetData();
|
|
if (ssoConfigurationData is { MemberDecryptionType: MemberDecryptionType.KeyConnector } &&
|
|
!string.IsNullOrEmpty(ssoConfigurationData.KeyConnectorUrl))
|
|
{
|
|
_options.KeyConnectorOption = new KeyConnectorUserDecryptionOption(ssoConfigurationData.KeyConnectorUrl);
|
|
}
|
|
}
|
|
|
|
private async Task BuildTrustedDeviceOptionsAsync()
|
|
{
|
|
// TrustedDeviceEncryption only exists for SSO, if that changes then these guards should change
|
|
if (_ssoConfig == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var isTdeActive = _ssoConfig.GetData() is
|
|
{ MemberDecryptionType: MemberDecryptionType.TrustedDeviceEncryption };
|
|
var isTdeOffboarding = !_user.HasMasterPassword() && _device != null && _device.IsTrusted() && !isTdeActive;
|
|
if (!isTdeActive && !isTdeOffboarding)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string? encryptedPrivateKey = null;
|
|
string? encryptedUserKey = null;
|
|
if (_device != null && _device.IsTrusted())
|
|
{
|
|
encryptedPrivateKey = _device.EncryptedPrivateKey;
|
|
encryptedUserKey = _device.EncryptedUserKey;
|
|
}
|
|
|
|
var hasLoginApprovingDevice = false;
|
|
if (_device != null)
|
|
{
|
|
var allDevices = await _deviceRepository.GetManyByUserIdAsync(_user.Id);
|
|
// Checks if the current user has any devices that are capable of approving login with device requests
|
|
// except for their current device.
|
|
hasLoginApprovingDevice = allDevices.Any(d =>
|
|
d.Identifier != _device.Identifier &&
|
|
_loginApprovingClientTypes.TypesThatCanApprove.Contains(DeviceTypes.ToClientType(d.Type)));
|
|
}
|
|
|
|
// Just-in-time-provisioned users, which can include users invited to a TDE organization with SSO and granted
|
|
// the Admin/Owner role or Custom user role with ManageResetPassword permission, will not have claims available
|
|
// in context to reflect this permission if granted as part of an invite for the current organization.
|
|
// Therefore, as written today, CurrentContext will not surface those permissions for those users.
|
|
// In order to make this check accurate at first login for all applicable cases, we have to go back to the
|
|
// database record.
|
|
// In the TDE flow, the users will have been JIT-provisioned at SSO callback time, and the relationship between
|
|
// user and organization user will have been codified.
|
|
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(_ssoConfig.OrganizationId, _user.Id);
|
|
var hasManageResetPasswordPermission = await EvaluateHasManageResetPasswordPermission();
|
|
|
|
// They are only able to be approved by an admin if they have enrolled is reset password
|
|
var hasAdminApproval = organizationUser != null && !string.IsNullOrEmpty(organizationUser.ResetPasswordKey);
|
|
|
|
_options.TrustedDeviceOption = new TrustedDeviceUserDecryptionOption(
|
|
hasAdminApproval,
|
|
hasLoginApprovingDevice,
|
|
hasManageResetPasswordPermission,
|
|
isTdeOffboarding,
|
|
encryptedPrivateKey,
|
|
encryptedUserKey);
|
|
return;
|
|
|
|
/// Determine if the user has manage reset password permission,
|
|
/// as post-SSO logic requires it for forcing users with this permission to set a password.
|
|
async Task<bool> EvaluateHasManageResetPasswordPermission()
|
|
{
|
|
if (organizationUser == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var organizationUserHasResetPasswordPermission =
|
|
// The repository will pull users in all statuses, so we also need to ensure that revoked-status users do not have
|
|
// permissions sent down.
|
|
organizationUser.Status is OrganizationUserStatusType.Invited or OrganizationUserStatusType.Accepted or
|
|
OrganizationUserStatusType.Confirmed &&
|
|
// Admins and owners get ManageResetPassword functionally "for free" through their role.
|
|
(organizationUser.Type is OrganizationUserType.Admin or OrganizationUserType.Owner ||
|
|
// Custom users can have the ManagePasswordReset permission assigned directly.
|
|
organizationUser.GetPermissions() is { ManageResetPassword: true });
|
|
|
|
return organizationUserHasResetPasswordPermission ||
|
|
// A provider user for the given organization gets ManageResetPassword through that relationship.
|
|
await _currentContext.ProviderUserForOrgAsync(_ssoConfig.OrganizationId);
|
|
}
|
|
}
|
|
|
|
private void BuildMasterPasswordUnlock()
|
|
{
|
|
if (_user.HasMasterPassword())
|
|
{
|
|
_options.HasMasterPassword = true;
|
|
_options.MasterPasswordUnlock = new MasterPasswordUnlockResponseModel
|
|
{
|
|
Kdf = new MasterPasswordUnlockKdfResponseModel
|
|
{
|
|
KdfType = _user.Kdf,
|
|
Iterations = _user.KdfIterations,
|
|
Memory = _user.KdfMemory,
|
|
Parallelism = _user.KdfParallelism
|
|
},
|
|
MasterKeyEncryptedUserKey = _user.Key!,
|
|
Salt = _user.Email.ToLowerInvariant()
|
|
};
|
|
}
|
|
else
|
|
{
|
|
_options.HasMasterPassword = false;
|
|
}
|
|
}
|
|
}
|