mirror of
https://github.com/bitwarden/server.git
synced 2025-07-07 02:52:50 -05:00
Trusted Device Encryption feature (#3151)
* [PM-1203] feat: allow verification for all passwordless accounts (#3038) * [PM-1033] Org invite user creation flow 1 (#3028) * [PM-1033] feat: remove user verification from password enrollment * [PM-1033] feat: auto accept invitation when enrolling into password reset * [PM-1033] fix: controller tests * [PM-1033] refactor: `UpdateUserResetPasswordEnrollmentCommand` * [PM-1033] refactor(wip): make `AcceptUserCommand` * Revert "[PM-1033] refactor(wip): make `AcceptUserCommand`" This reverts commitdc1319e7fa
. * Revert "[PM-1033] refactor: `UpdateUserResetPasswordEnrollmentCommand`" This reverts commit43df689c7f
. * [PM-1033] refactor: move invite accept to controller This avoids creating yet another method that depends on having `IUserService` passed in as a parameter * [PM-1033] fix: add missing changes * [PM-1381] Add Trusted Device Keys to Auth Response (#3066) * Return Keys for Trusted Device - Check whether the current logging in device is trusted - Return their keys on successful login * Formatting * Address PR Feedback * Add Remarks Comment * [PM-1338] `AuthRequest` Event Logs (#3046) * Update AuthRequestController - Only allow AdminApproval Requests to be created from authed endpoint - Add endpoint that has authentication to be able to create admin approval * Add PasswordlessAuthSettings - Add settings for customizing expiration times * Add new EventTypes * Add Logic for AdminApproval Type - Add logic for validating AdminApproval expiration - Add event logging for Approval/Disapproval of AdminApproval - Add logic for creating AdminApproval types * Add Test Helpers - Change BitAutoData to allow you to use string representations of common types. * Add/Update AuthRequestService Tests * Run Formatting * Switch to 7 Days * Add Test Covering ResponseDate Being Set * Address PR Feedback - Create helper for checking if date is expired - Move validation logic into smaller methods * Switch to User Event Type - Make RequestDeviceApproval user type - User types will log for each org user is in * [PM-2998] Move Approving Device Check (#3101) * Move Check for Approving Devices - Exclude currently logging in device - Remove old way of checking - Add tests asserting behavior * Update DeviceType list * Update Naming & Address PR Feedback * Fix Tests * Address PR Feedback * Formatting * Now Fully Update Naming? * Feature/auth/pm 2759/add can reset password to user decryption options (#3113) * PM-2759 - BaseRequestValidator.cs - CreateUserDecryptionOptionsAsync - Add new hasManageResetPasswordPermission for post SSO redirect logic required on client. * PM-2759 - Update IdentityServerSsoTests.cs to all pass based on the addition of HasManageResetPasswordPermission to TrustedDeviceUserDecryptionOption * IdentityServerSsoTests.cs - fix typo in test name: LoggingApproval --> LoginApproval * PM1259 - Add test case for verifying that TrustedDeviceOption.hasManageResetPasswordPermission is set properly based on user permission * dotnet format run * Feature/auth/pm 2759/add can reset password to user decryption options fix jit users (#3120) * PM-2759 - IdentityServer - CreateUserDecryptionOptionsAsync - hasManageResetPasswordPermission set logic was broken for JIT provisioned users as I assumed we would always have a list of at least 1 org during the SSO process. Added TODO for future test addition but getting this out there now as QA is blocked by being unable to create JIT provisioned users. * dotnet format * Tiny tweak * [PM-1339] Allow Rotating Device Keys (#3096) * Allow Rotation of Trusted Device Keys - Add endpoint for getting keys relating to rotation - Add endpoint for rotating your current device - In the same endpoint allow a list of other devices to rotate * Formatting * Use Extension Method * Add Tests from PR Co-authored-by: Jared Snider <jsnider@bitwarden.com> --------- Co-authored-by: Jared Snider <jsnider@bitwarden.com> * Check the user directly if they have the ResetPasswordKey (#3153) * PM-3327 - UpdateKeyAsync must exempt the currently calling device from the logout notification in order to prevent prematurely logging the user out before the client side key rotation process can complete. The calling device will log itself out once it is done. (#3170) * Allow OTP Requests When Users Are On TDE (#3184) * [PM-3356][PM-3292] Allow OTP For All (#3188) * Allow OTP For All - On a trusted device isn't a good check because a user might be using a trusted device locally but not trusted it long term - The logic wasn't working for KC users anyways * Remove Old Comment * [AC-1601] Added RequireSso policy as a dependency of TDE (#3209) * Added RequireSso policy as a dependency of TDE. * Added test for RequireSso for TDE. * Added save. * Fixed policy name. --------- Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> Co-authored-by: Vincent Salucci <vincesalucci21@gmail.com> Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Co-authored-by: Jared Snider <jsnider@bitwarden.com>
This commit is contained in:
@ -0,0 +1,21 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Api.Request;
|
||||
|
||||
public class OtherDeviceKeysUpdateRequestModel : DeviceKeysUpdateRequestModel
|
||||
{
|
||||
[Required]
|
||||
public Guid DeviceId { get; set; }
|
||||
}
|
||||
|
||||
public class DeviceKeysUpdateRequestModel
|
||||
{
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
public string EncryptedPublicKey { get; set; }
|
||||
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
public string EncryptedUserKey { get; set; }
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Api.Response;
|
||||
|
||||
public class ProtectedDeviceResponseModel : ResponseModel
|
||||
{
|
||||
public ProtectedDeviceResponseModel(Device device)
|
||||
: base("protectedDevice")
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(device);
|
||||
|
||||
Id = device.Id;
|
||||
Name = device.Name;
|
||||
Type = device.Type;
|
||||
Identifier = device.Identifier;
|
||||
CreationDate = device.CreationDate;
|
||||
EncryptedUserKey = device.EncryptedUserKey;
|
||||
EncryptedPublicKey = device.EncryptedPublicKey;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public DeviceType Type { get; set; }
|
||||
public string Identifier { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
public string EncryptedUserKey { get; set; }
|
||||
public string EncryptedPublicKey { get; set; }
|
||||
}
|
@ -32,10 +32,22 @@ public class UserDecryptionOptions : ResponseModel
|
||||
public class TrustedDeviceUserDecryptionOption
|
||||
{
|
||||
public bool HasAdminApproval { get; }
|
||||
public bool HasLoginApprovingDevice { get; }
|
||||
public bool HasManageResetPasswordPermission { get; }
|
||||
public string? EncryptedPrivateKey { get; }
|
||||
public string? EncryptedUserKey { get; }
|
||||
|
||||
public TrustedDeviceUserDecryptionOption(bool hasAdminApproval)
|
||||
public TrustedDeviceUserDecryptionOption(bool hasAdminApproval,
|
||||
bool hasLoginApprovingDevice,
|
||||
bool hasManageResetPasswordPermission,
|
||||
string? encryptedPrivateKey,
|
||||
string? encryptedUserKey)
|
||||
{
|
||||
HasAdminApproval = hasAdminApproval;
|
||||
HasLoginApprovingDevice = hasLoginApprovingDevice;
|
||||
HasManageResetPasswordPermission = hasManageResetPasswordPermission;
|
||||
EncryptedPrivateKey = encryptedPrivateKey;
|
||||
EncryptedUserKey = encryptedUserKey;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,11 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using System.Diagnostics;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Exceptions;
|
||||
using Bit.Core.Auth.Models.Api.Request.AuthRequest;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -21,6 +24,8 @@ public class AuthRequestService : IAuthRequestService
|
||||
private readonly IDeviceRepository _deviceRepository;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
|
||||
public AuthRequestService(
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
@ -28,7 +33,9 @@ public class AuthRequestService : IAuthRequestService
|
||||
IGlobalSettings globalSettings,
|
||||
IDeviceRepository deviceRepository,
|
||||
ICurrentContext currentContext,
|
||||
IPushNotificationService pushNotificationService)
|
||||
IPushNotificationService pushNotificationService,
|
||||
IEventService eventService,
|
||||
IOrganizationUserRepository organizationRepository)
|
||||
{
|
||||
_authRequestRepository = authRequestRepository;
|
||||
_userRepository = userRepository;
|
||||
@ -36,6 +43,8 @@ public class AuthRequestService : IAuthRequestService
|
||||
_deviceRepository = deviceRepository;
|
||||
_currentContext = currentContext;
|
||||
_pushNotificationService = pushNotificationService;
|
||||
_eventService = eventService;
|
||||
_organizationUserRepository = organizationRepository;
|
||||
}
|
||||
|
||||
public async Task<AuthRequest?> GetAuthRequestAsync(Guid id, Guid userId)
|
||||
@ -52,9 +61,12 @@ public class AuthRequestService : IAuthRequestService
|
||||
public async Task<AuthRequest?> GetValidatedAuthRequestAsync(Guid id, string code)
|
||||
{
|
||||
var authRequest = await _authRequestRepository.GetByIdAsync(id);
|
||||
if (authRequest == null ||
|
||||
!CoreHelpers.FixedTimeEquals(authRequest.AccessCode, code) ||
|
||||
authRequest.GetExpirationDate() < DateTime.UtcNow)
|
||||
if (authRequest == null || !CoreHelpers.FixedTimeEquals(authRequest.AccessCode, code))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!IsAuthRequestValid(authRequest))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@ -91,6 +103,42 @@ public class AuthRequestService : IAuthRequestService
|
||||
}
|
||||
}
|
||||
|
||||
// AdminApproval requests require correlating the user and their organization
|
||||
if (model.Type == AuthRequestType.AdminApproval)
|
||||
{
|
||||
// TODO: When single org policy is turned on we should query for only a single organization from the current user
|
||||
// and create only an AuthRequest for that organization and return only that one
|
||||
|
||||
// This will send out the request to all organizations this user belongs to
|
||||
var organizationUsers = await _organizationUserRepository.GetManyByUserAsync(_currentContext.UserId!.Value);
|
||||
|
||||
if (organizationUsers.Count == 0)
|
||||
{
|
||||
throw new BadRequestException("User does not belong to any organizations.");
|
||||
}
|
||||
|
||||
// A user event will automatically create logs for each organization/provider this user belongs to.
|
||||
await _eventService.LogUserEventAsync(user.Id, EventType.User_RequestedDeviceApproval);
|
||||
|
||||
AuthRequest? firstAuthRequest = null;
|
||||
foreach (var organizationUser in organizationUsers)
|
||||
{
|
||||
var createdAuthRequest = await CreateAuthRequestAsync(model, user, organizationUser.OrganizationId);
|
||||
firstAuthRequest ??= createdAuthRequest;
|
||||
}
|
||||
|
||||
// I know this won't be null because I have already validated that at least one organization exists
|
||||
return firstAuthRequest!;
|
||||
}
|
||||
|
||||
var authRequest = await CreateAuthRequestAsync(model, user, organizationId: null);
|
||||
await _pushNotificationService.PushAuthRequestAsync(authRequest);
|
||||
return authRequest;
|
||||
}
|
||||
|
||||
private async Task<AuthRequest> CreateAuthRequestAsync(AuthRequestCreateRequestModel model, User user, Guid? organizationId)
|
||||
{
|
||||
Debug.Assert(_currentContext.DeviceType.HasValue, "DeviceType should have already been validated to have a value.");
|
||||
var authRequest = new AuthRequest
|
||||
{
|
||||
RequestDeviceIdentifier = model.DeviceIdentifier,
|
||||
@ -100,35 +148,58 @@ public class AuthRequestService : IAuthRequestService
|
||||
PublicKey = model.PublicKey,
|
||||
UserId = user.Id,
|
||||
Type = model.Type.GetValueOrDefault(),
|
||||
OrganizationId = organizationId,
|
||||
};
|
||||
|
||||
authRequest = await _authRequestRepository.CreateAsync(authRequest);
|
||||
await _pushNotificationService.PushAuthRequestAsync(authRequest);
|
||||
return authRequest;
|
||||
}
|
||||
|
||||
public async Task<AuthRequest> UpdateAuthRequestAsync(Guid authRequestId, Guid userId, AuthRequestUpdateRequestModel model)
|
||||
public async Task<AuthRequest> UpdateAuthRequestAsync(Guid authRequestId, Guid currentUserId, AuthRequestUpdateRequestModel model)
|
||||
{
|
||||
var authRequest = await _authRequestRepository.GetByIdAsync(authRequestId);
|
||||
if (authRequest == null || authRequest.UserId != userId || authRequest.GetExpirationDate() < DateTime.UtcNow)
|
||||
|
||||
if (authRequest == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Once Approval/Disapproval has been set, this AuthRequest should not be updated again.
|
||||
if (authRequest.Approved is not null)
|
||||
{
|
||||
throw new DuplicateAuthRequestException();
|
||||
}
|
||||
|
||||
// Admin approval responses are not tied to a specific device, so we don't need to validate it.
|
||||
if (authRequest.Type != AuthRequestType.AdminApproval)
|
||||
// Do type specific validation
|
||||
switch (authRequest.Type)
|
||||
{
|
||||
var device = await _deviceRepository.GetByIdentifierAsync(model.DeviceIdentifier, userId);
|
||||
if (device == null)
|
||||
{
|
||||
throw new BadRequestException("Invalid device.");
|
||||
}
|
||||
authRequest.ResponseDeviceId = device.Id;
|
||||
case AuthRequestType.AdminApproval:
|
||||
// AdminApproval has a different expiration time, by default is 7 days compared to
|
||||
// non-AdminApproval ones having a default of 15 minutes.
|
||||
if (IsDateExpired(authRequest.CreationDate, _globalSettings.PasswordlessAuth.AdminRequestExpiration))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
break;
|
||||
case AuthRequestType.AuthenticateAndUnlock:
|
||||
case AuthRequestType.Unlock:
|
||||
if (IsDateExpired(authRequest.CreationDate, _globalSettings.PasswordlessAuth.UserRequestExpiration))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (authRequest.UserId != currentUserId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Admin approval responses are not tied to a specific device, but these types are so we need to validate them
|
||||
var device = await _deviceRepository.GetByIdentifierAsync(model.DeviceIdentifier, currentUserId);
|
||||
if (device == null)
|
||||
{
|
||||
throw new BadRequestException("Invalid device.");
|
||||
}
|
||||
authRequest.ResponseDeviceId = device.Id;
|
||||
break;
|
||||
}
|
||||
|
||||
authRequest.ResponseDate = DateTime.UtcNow;
|
||||
@ -146,9 +217,55 @@ public class AuthRequestService : IAuthRequestService
|
||||
// to not leak that it was denied to the originating client if it was originated by a malicious actor.
|
||||
if (authRequest.Approved ?? true)
|
||||
{
|
||||
if (authRequest.OrganizationId.HasValue)
|
||||
{
|
||||
var organizationUser = await _organizationUserRepository
|
||||
.GetByOrganizationAsync(authRequest.OrganizationId.Value, authRequest.UserId);
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_ApprovedAuthRequest);
|
||||
}
|
||||
|
||||
// No matter what we want to push out the success notification
|
||||
await _pushNotificationService.PushAuthRequestResponseAsync(authRequest);
|
||||
}
|
||||
// If the request is rejected by an organization admin then we want to log an event of that action
|
||||
else if (authRequest.Approved.HasValue && !authRequest.Approved.Value && authRequest.OrganizationId.HasValue)
|
||||
{
|
||||
var organizationUser = await _organizationUserRepository
|
||||
.GetByOrganizationAsync(authRequest.OrganizationId.Value, authRequest.UserId);
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_RejectedAuthRequest);
|
||||
}
|
||||
|
||||
return authRequest;
|
||||
}
|
||||
|
||||
private bool IsAuthRequestValid(AuthRequest authRequest)
|
||||
{
|
||||
return authRequest.Type switch
|
||||
{
|
||||
AuthRequestType.AuthenticateAndUnlock or AuthRequestType.Unlock
|
||||
=> !IsDateExpired(authRequest.CreationDate, _globalSettings.PasswordlessAuth.UserRequestExpiration),
|
||||
AuthRequestType.AdminApproval => IsAdminApprovalAuthRequestValid(authRequest),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private bool IsAdminApprovalAuthRequestValid(AuthRequest authRequest)
|
||||
{
|
||||
Debug.Assert(authRequest.Type == AuthRequestType.AdminApproval, "This method should only be called on AdminApproval type");
|
||||
// If an AdminApproval type has been approved it's expiration time is based on how long it's been since approved.
|
||||
if (authRequest.Approved is true)
|
||||
{
|
||||
Debug.Assert(authRequest.ResponseDate.HasValue, "The response date should have been set when the request was updated.");
|
||||
return !IsDateExpired(authRequest.ResponseDate.Value, _globalSettings.PasswordlessAuth.AfterAdminApprovalExpiration);
|
||||
}
|
||||
else
|
||||
{
|
||||
return !IsDateExpired(authRequest.CreationDate, _globalSettings.PasswordlessAuth.AdminRequestExpiration);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsDateExpired(DateTime savedDate, TimeSpan allowedLifetime)
|
||||
{
|
||||
return DateTime.UtcNow > savedDate.Add(allowedLifetime);
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ public class SsoConfigService : ISsoConfigService
|
||||
throw new BadRequestException("Key Connector cannot be disabled at this moment.");
|
||||
}
|
||||
|
||||
// Automatically enable account recovery and single org policies if trusted device encryption is selected
|
||||
// Automatically enable account recovery, SSO required, and single org policies if trusted device encryption is selected
|
||||
if (config.GetData().MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption)
|
||||
{
|
||||
var singleOrgPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.SingleOrg) ??
|
||||
@ -78,8 +78,13 @@ public class SsoConfigService : ISsoConfigService
|
||||
|
||||
resetPolicy.Enabled = true;
|
||||
resetPolicy.SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true });
|
||||
|
||||
await _policyService.SaveAsync(resetPolicy, _userService, _organizationService, null);
|
||||
|
||||
var ssoRequiredPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.RequireSso) ??
|
||||
new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.RequireSso, };
|
||||
|
||||
ssoRequiredPolicy.Enabled = true;
|
||||
await _policyService.SaveAsync(ssoRequiredPolicy, _userService, _organizationService, null);
|
||||
}
|
||||
|
||||
await LogEventsAsync(config, oldConfig);
|
||||
|
23
src/Core/Auth/Utilities/DeviceExtensions.cs
Normal file
23
src/Core/Auth/Utilities/DeviceExtensions.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Auth.Utilities;
|
||||
|
||||
public static class DeviceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a boolean representing if the device has enough information on it to determine whether or not it is trusted.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It is possible for a device to be un-trusted client side and not notify the server side. This should not be
|
||||
/// the source of truth for whether a device is fully trusted and should just be considered that, to the server,
|
||||
/// a device has the necessary information to be "trusted".
|
||||
/// </remarks>
|
||||
public static bool IsTrusted(this Device device)
|
||||
{
|
||||
return !string.IsNullOrEmpty(device.EncryptedUserKey) &&
|
||||
!string.IsNullOrEmpty(device.EncryptedPublicKey) &&
|
||||
!string.IsNullOrEmpty(device.EncryptedPrivateKey);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user