1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 23: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 commit dc1319e7fa.

* Revert "[PM-1033] refactor: `UpdateUserResetPasswordEnrollmentCommand`"

This reverts commit 43df689c7f.

* [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:
Todd Martin
2023-08-17 16:03:06 -04:00
committed by GitHub
parent fc814ff352
commit 1c3afcdffc
33 changed files with 1666 additions and 254 deletions

View File

@ -1,5 +1,6 @@
using Bit.Api.Auth.Models.Response;
using Bit.Api.Models.Response;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Api.Request.AuthRequest;
using Bit.Core.Auth.Services;
using Bit.Core.Exceptions;
@ -72,6 +73,18 @@ public class AuthRequestsController : Controller
[HttpPost("")]
[AllowAnonymous]
public async Task<AuthRequestResponseModel> Post([FromBody] AuthRequestCreateRequestModel model)
{
if (model.Type == AuthRequestType.AdminApproval)
{
throw new BadRequestException("You must be authenticated to create a request of that type.");
}
var authRequest = await _authRequestService.CreateAuthRequestAsync(model);
var r = new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);
return r;
}
[HttpPost("admin-request")]
public async Task<AuthRequestResponseModel> PostAdminRequest([FromBody] AuthRequestCreateRequestModel model)
{
var authRequest = await _authRequestService.CreateAuthRequestAsync(model);
var r = new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);

View File

@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Core.Auth.Models.Api.Request;
#nullable enable
namespace Bit.Api.Auth.Models.Request;
public class UpdateDevicesTrustRequestModel : SecretVerificationRequestModel
{
[Required]
public DeviceKeysUpdateRequestModel CurrentDevice { get; set; } = null!;
public IEnumerable<OtherDeviceKeysUpdateRequestModel>? OtherDevices { get; set; }
}

View File

@ -879,10 +879,6 @@ public class AccountsController : Controller
public async Task PostRequestOTP()
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user is not { UsesKeyConnector: true })
{
throw new UnauthorizedAccessException();
}
await _userService.SendOTPAsync(user);
}
@ -891,10 +887,6 @@ public class AccountsController : Controller
public async Task VerifyOTP([FromBody] VerifyOTPRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user is not { UsesKeyConnector: true })
{
throw new UnauthorizedAccessException();
}
if (!await _userService.VerifyOTPAsync(user, model.OTP))
{

View File

@ -1,7 +1,11 @@
using Bit.Api.Models.Request;
using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Models.Request;
using Bit.Api.Models.Response;
using Bit.Core.Auth.Models.Api.Request;
using Bit.Core.Auth.Models.Api.Response;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -19,17 +23,20 @@ public class DevicesController : Controller
private readonly IDeviceService _deviceService;
private readonly IUserService _userService;
private readonly IUserRepository _userRepository;
private readonly ICurrentContext _currentContext;
public DevicesController(
IDeviceRepository deviceRepository,
IDeviceService deviceService,
IUserService userService,
IUserRepository userRepository)
IUserRepository userRepository,
ICurrentContext currentContext)
{
_deviceRepository = deviceRepository;
_deviceService = deviceService;
_userService = userService;
_userRepository = userRepository;
_currentContext = currentContext;
}
[HttpGet("{id}")]
@ -66,15 +73,6 @@ public class DevicesController : Controller
return new ListResponseModel<DeviceResponseModel>(responses);
}
[HttpPost("exist-by-types")]
public async Task<ActionResult<bool>> GetExistenceByTypes([FromBody] DeviceType[] deviceTypes)
{
var userId = _userService.GetProperUserId(User).Value;
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
var userHasDeviceOfTypes = devices.Any(d => deviceTypes.Contains(d.Type));
return Ok(userHasDeviceOfTypes);
}
[HttpPost("")]
public async Task<DeviceResponseModel> Post([FromBody] DeviceRequestModel model)
{
@ -117,6 +115,55 @@ public class DevicesController : Controller
return response;
}
[HttpPost("{identifier}/retrieve-keys")]
public async Task<ProtectedDeviceResponseModel> GetDeviceKeys(string identifier, [FromBody] SecretVerificationRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
if (!await _userService.VerifySecretAsync(user, model.Secret))
{
await Task.Delay(2000);
throw new BadRequestException(string.Empty, "User verification failed.");
}
var device = await _deviceRepository.GetByIdentifierAsync(identifier, user.Id);
if (device == null)
{
throw new NotFoundException();
}
return new ProtectedDeviceResponseModel(device);
}
[HttpPost("update-trust")]
public async Task PostUpdateTrust([FromBody] UpdateDevicesTrustRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
if (!await _userService.VerifySecretAsync(user, model.Secret))
{
await Task.Delay(2000);
throw new BadRequestException(string.Empty, "User verification failed.");
}
await _deviceService.UpdateDevicesTrustAsync(
_currentContext.DeviceIdentifier,
user.Id,
model.CurrentDevice,
model.OtherDevices ?? Enumerable.Empty<OtherDeviceKeysUpdateRequestModel>());
}
[HttpPut("identifier/{identifier}/token")]
[HttpPost("identifier/{identifier}/token")]
public async Task PutToken(string identifier, [FromBody] DeviceTokenRequestModel model)

View File

@ -2,7 +2,6 @@
using Bit.Api.Models.Response;
using Bit.Api.Models.Response.Organizations;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
@ -313,7 +312,7 @@ public class OrganizationUsersController : Controller
}
[HttpPut("{userId}/reset-password-enrollment")]
public async Task PutResetPasswordEnrollment(string orgId, string userId, [FromBody] OrganizationUserResetPasswordEnrollmentRequestModel model)
public async Task PutResetPasswordEnrollment(Guid orgId, Guid userId, [FromBody] OrganizationUserResetPasswordEnrollmentRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
@ -321,16 +320,14 @@ public class OrganizationUsersController : Controller
throw new UnauthorizedAccessException();
}
if (model.ResetPasswordKey != null && !await _userService.VerifySecretAsync(user, model.Secret))
var callingUserId = user.Id;
await _organizationService.UpdateUserResetPasswordEnrollmentAsync(
orgId, userId, model.ResetPasswordKey, callingUserId);
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(orgId, user.Id);
if (orgUser.Status == OrganizationUserStatusType.Invited)
{
await Task.Delay(2000);
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
}
else
{
var callingUserId = user.Id;
await _organizationService.UpdateUserResetPasswordEnrollmentAsync(
new Guid(orgId), new Guid(userId), model.ResetPasswordKey, callingUserId);
await _organizationService.AcceptUserAsync(orgId, user, _userService);
}
}
@ -466,7 +463,7 @@ public class OrganizationUsersController : Controller
private async Task RestoreOrRevokeUserAsync(
Guid orgId,
Guid id,
Func<OrganizationUser, Guid?, Task> statusAction)
Func<Core.Entities.OrganizationUser, Guid?, Task> statusAction)
{
if (!await _currentContext.ManageUsers(orgId))
{
@ -486,7 +483,7 @@ public class OrganizationUsersController : Controller
private async Task<ListResponseModel<OrganizationUserBulkResponseModel>> RestoreOrRevokeUsersAsync(
Guid orgId,
OrganizationUserBulkRequestModel model,
Func<Guid, IEnumerable<Guid>, Guid?, Task<List<Tuple<OrganizationUser, string>>>> statusAction)
Func<Guid, IEnumerable<Guid>, Guid?, Task<List<Tuple<Core.Entities.OrganizationUser, string>>>> statusAction)
{
if (!await _currentContext.ManageUsers(orgId))
{

View File

@ -1,5 +1,4 @@
using System.ComponentModel.DataAnnotations;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
@ -108,7 +107,7 @@ public class OrganizationUserUpdateGroupsRequestModel
public IEnumerable<string> GroupIds { get; set; }
}
public class OrganizationUserResetPasswordEnrollmentRequestModel : SecretVerificationRequestModel
public class OrganizationUserResetPasswordEnrollmentRequestModel
{
public string ResetPasswordKey { get; set; }
}

View File

@ -1,4 +1,5 @@
using Bit.Core.Entities;
using Bit.Core.Auth.Utilities;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Api;
@ -19,9 +20,7 @@ public class DeviceResponseModel : ResponseModel
Type = device.Type;
Identifier = device.Identifier;
CreationDate = device.CreationDate;
EncryptedUserKey = device.EncryptedUserKey;
EncryptedPublicKey = device.EncryptedPublicKey;
EncryptedPrivateKey = device.EncryptedPrivateKey;
IsTrusted = device.IsTrusted();
}
public Guid Id { get; set; }
@ -29,7 +28,5 @@ public class DeviceResponseModel : ResponseModel
public DeviceType Type { get; set; }
public string Identifier { get; set; }
public DateTime CreationDate { get; set; }
public string EncryptedUserKey { get; }
public string EncryptedPublicKey { get; }
public string EncryptedPrivateKey { get; }
public bool IsTrusted { get; set; }
}

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);

View 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);
}
}

View File

@ -213,4 +213,9 @@ public class User : ITableObject<Guid>, ISubscriber, IStorable, IStorableSubscri
SecurityStamp = SecurityStamp
};
}
public bool HasMasterPassword()
{
return MasterPassword != null;
}
}

View File

@ -13,6 +13,7 @@ public enum EventType : int
User_ClientExportedVault = 1007,
User_UpdatedTempPassword = 1008,
User_MigratedKeyToKeyConnector = 1009,
User_RequestedDeviceApproval = 1010,
Cipher_Created = 1100,
Cipher_Updated = 1101,
@ -54,6 +55,8 @@ public enum EventType : int
OrganizationUser_FirstSsoLogin = 1510,
OrganizationUser_Revoked = 1511,
OrganizationUser_Restored = 1512,
OrganizationUser_ApprovedAuthRequest = 1513,
OrganizationUser_RejectedAuthRequest = 1514,
Organization_Updated = 1600,
Organization_PurgedVault = 1601,

View File

@ -1,4 +1,5 @@
using Bit.Core.Entities;
using Bit.Core.Auth.Models.Api.Request;
using Bit.Core.Entities;
namespace Bit.Core.Services;
@ -7,4 +8,8 @@ public interface IDeviceService
Task SaveAsync(Device device);
Task ClearTokenAsync(Device device);
Task DeleteAsync(Device device);
Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,
Guid currentUserId,
DeviceKeysUpdateRequestModel currentDeviceUpdate,
IEnumerable<OtherDeviceKeysUpdateRequestModel> alteredDevices);
}

View File

@ -41,6 +41,7 @@ public interface IOrganizationService
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false);
Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token, IUserService userService);
Task<OrganizationUser> AcceptUserAsync(string orgIdentifier, User user, IUserService userService);
Task<OrganizationUser> AcceptUserAsync(Guid organizationId, User user, IUserService userService);
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
Guid confirmingUserId, IUserService userService);
Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,

View File

@ -1,4 +1,7 @@
using Bit.Core.Entities;
using Bit.Core.Auth.Models.Api.Request;
using Bit.Core.Auth.Utilities;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
namespace Bit.Core.Services;
@ -43,4 +46,61 @@ public class DeviceService : IDeviceService
await _deviceRepository.DeleteAsync(device);
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
}
public async Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,
Guid currentUserId,
DeviceKeysUpdateRequestModel currentDeviceUpdate,
IEnumerable<OtherDeviceKeysUpdateRequestModel> alteredDevices)
{
var existingDevices = await _deviceRepository.GetManyByUserIdAsync(currentUserId);
var currentDevice = existingDevices.FirstOrDefault(d => d.Identifier == currentDeviceIdentifier);
if (currentDevice == null)
{
throw new NotFoundException();
}
existingDevices.Remove(currentDevice);
var alterDeviceKeysDict = alteredDevices.ToDictionary(d => d.DeviceId);
if (alterDeviceKeysDict.ContainsKey(currentDevice.Id))
{
throw new BadRequestException("Current device can not be an optional rotation.");
}
currentDevice.EncryptedPublicKey = currentDeviceUpdate.EncryptedPublicKey;
currentDevice.EncryptedUserKey = currentDeviceUpdate.EncryptedUserKey;
await _deviceRepository.UpsertAsync(currentDevice);
foreach (var device in existingDevices)
{
if (!device.IsTrusted())
{
// You can't update the trust of a device that isn't trusted to begin with
// should we throw and consider this a BadRequest? If we want to consider it a invalid request
// we need to check that information before we enter this foreach, we don't want to partially complete
// this process.
continue;
}
if (alterDeviceKeysDict.TryGetValue(device.Id, out var updateRequest))
{
// An update to this device was requested
device.EncryptedPublicKey = updateRequest.EncryptedPublicKey;
device.EncryptedUserKey = updateRequest.EncryptedUserKey;
}
else
{
// No update to this device requested, just untrust it
device.EncryptedUserKey = null;
device.EncryptedPublicKey = null;
device.EncryptedPrivateKey = null;
}
await _deviceRepository.UpsertAsync(device);
}
}
}

View File

@ -1114,6 +1114,24 @@ public class OrganizationService : IOrganizationService
return await AcceptUserAsync(orgUser, user, userService);
}
public async Task<OrganizationUser> AcceptUserAsync(Guid organizationId, User user, IUserService userService)
{
var org = await _organizationRepository.GetByIdAsync(organizationId);
if (org == null)
{
throw new BadRequestException("Organization invalid.");
}
var usersOrgs = await _organizationUserRepository.GetManyByUserAsync(user.Id);
var orgUser = usersOrgs.FirstOrDefault(u => u.OrganizationId == org.Id);
if (orgUser == null)
{
throw new BadRequestException("User not found within organization.");
}
return await AcceptUserAsync(orgUser, user, userService);
}
private async Task<OrganizationUser> AcceptUserAsync(OrganizationUser orgUser, User user,
IUserService userService)
{

View File

@ -73,6 +73,7 @@ public class PolicyService : IPolicyService
else
{
await RequiredByKeyConnectorAsync(org);
await RequiredBySsoTrustedDeviceEncryptionAsync(org);
}
break;

View File

@ -892,7 +892,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
await _userRepository.ReplaceAsync(user);
}
await _pushService.PushLogOutAsync(user.Id);
await _pushService.PushLogOutAsync(user.Id, excludeCurrentContextFromPush: true);
return IdentityResult.Success;
}
@ -1455,26 +1455,35 @@ public class UserService : UserManager<User>, IUserService, IDisposable
throw new BadRequestException("No user email.");
}
if (!user.UsesKeyConnector)
{
throw new BadRequestException("Not using Key Connector.");
}
var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultEmailProvider,
"otp:" + user.Email);
await _mailService.SendOTPEmailAsync(user.Email, token);
}
public Task<bool> VerifyOTPAsync(User user, string token)
public async Task<bool> VerifyOTPAsync(User user, string token)
{
return base.VerifyUserTokenAsync(user, TokenOptions.DefaultEmailProvider,
return await base.VerifyUserTokenAsync(user, TokenOptions.DefaultEmailProvider,
"otp:" + user.Email, token);
}
public async Task<bool> VerifySecretAsync(User user, string secret)
{
return user.UsesKeyConnector
? await VerifyOTPAsync(user, secret)
: await CheckPasswordAsync(user, secret);
bool isVerified;
if (user.HasMasterPassword())
{
// If the user has a master password the secret is most likely going to be a hash
// of their password, but in certain scenarios, like when the user has logged into their
// device without a password (trusted device encryption) but the account
// does still have a password we will allow the use of OTP.
isVerified = await CheckPasswordAsync(user, secret) ||
await VerifyOTPAsync(user, secret);
}
else
{
// If they don't have a password at all they can only do OTP
isVerified = await VerifyOTPAsync(user, secret);
}
return isVerified;
}
}

View File

@ -0,0 +1,21 @@
using Bit.Core.Enums;
namespace Bit.Core.Utilities;
public static class DeviceTypes
{
public static IReadOnlyCollection<DeviceType> MobileTypes { get; } = new[]
{
DeviceType.Android,
DeviceType.iOS,
DeviceType.AndroidAmazon,
};
public static IReadOnlyCollection<DeviceType> DesktopTypes { get; } = new[]
{
DeviceType.LinuxDesktop,
DeviceType.MacOsDesktop,
DeviceType.WindowsDesktop,
DeviceType.UWP,
};
}

View File

@ -3,13 +3,14 @@ using System.Reflection;
using System.Security.Claims;
using System.Text.Json;
using Bit.Core;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models;
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.Auth.Utilities;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
@ -22,6 +23,7 @@ using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Core.Utilities;
using Bit.Identity.Utilities;
using IdentityServer4.Validation;
using Microsoft.AspNetCore.Identity;
@ -207,7 +209,7 @@ public abstract class BaseRequestValidator<T> where T : class
customResponse.Add("KdfIterations", user.KdfIterations);
customResponse.Add("KdfMemory", user.KdfMemory);
customResponse.Add("KdfParallelism", user.KdfParallelism);
customResponse.Add("UserDecryptionOptions", await CreateUserDecryptionOptionsAsync(user, GetSubject(context)));
customResponse.Add("UserDecryptionOptions", await CreateUserDecryptionOptionsAsync(user, device, GetSubject(context)));
if (sendRememberToken)
{
@ -350,7 +352,8 @@ public abstract class BaseRequestValidator<T> where T : class
return true;
}
// Check if user belongs to any organization with an active SSO policy
// Check if user belongs to any organization with an active SSO policy
var anySsoPoliciesApplicableToUser = await PolicyService.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
if (anySsoPoliciesApplicableToUser)
{
@ -587,15 +590,17 @@ public abstract class BaseRequestValidator<T> where T : class
/// <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(User user, ClaimsPrincipal subject)
private async Task<UserDecryptionOptions> CreateUserDecryptionOptionsAsync(User user, Device device, ClaimsPrincipal subject)
{
var ssoConfigurationData = await GetSsoConfigurationDataAsync(subject);
var ssoConfiguration = await GetSsoConfigurationDataAsync(subject);
var userDecryptionOption = new UserDecryptionOptions
{
HasMasterPassword = !string.IsNullOrEmpty(user.MasterPassword)
};
var ssoConfigurationData = ssoConfiguration?.GetData();
if (ssoConfigurationData is { MemberDecryptionType: MemberDecryptionType.KeyConnector } && !string.IsNullOrEmpty(ssoConfigurationData.KeyConnectorUrl))
{
// KeyConnector makes it mutually exclusive
@ -606,15 +611,51 @@ public abstract class BaseRequestValidator<T> where T : class
// Only add the trusted device specific option when the flag is turned on
if (FeatureService.IsEnabled(FeatureFlagKeys.TrustedDeviceEncryption, CurrentContext) && ssoConfigurationData is { MemberDecryptionType: MemberDecryptionType.TrustedDeviceEncryption })
{
var hasAdminApproval = await PolicyService.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.ResetPassword);
string? encryptedPrivateKey = null;
string? encryptedUserKey = null;
if (device.IsTrusted())
{
encryptedPrivateKey = device.EncryptedPrivateKey;
encryptedUserKey = device.EncryptedUserKey;
}
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.
// NOTE: this doesn't check for if the users have configured the devices to be capable of approving requests as that is a client side setting.
var hasLoginApprovingDevice = allDevices
.Where(d => d.Identifier != device.Identifier && LoginApprovingDeviceTypes.Types.Contains(d.Type))
.Any();
// Determine if user has manage reset password permission as post sso logic requires it for forcing users with this permission to set a MP
var hasManageResetPasswordPermission = false;
// when a user is being created via JIT provisioning, they will not have any orgs so we can't assume we will have orgs here
if (CurrentContext.Organizations.Any(o => o.Id == ssoConfiguration!.OrganizationId))
{
// TDE requires single org so grabbing first org & id is fine.
hasManageResetPasswordPermission = await CurrentContext.ManageResetPassword(ssoConfiguration!.OrganizationId);
}
// If sso configuration data is not null then I know for sure that ssoConfiguration isn't null
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(ssoConfiguration!.OrganizationId, user.Id);
// They are only able to be approved by an admin if they have enrolled is reset password
var hasAdminApproval = !string.IsNullOrEmpty(organizationUser.ResetPasswordKey);
// TrustedDeviceEncryption only exists for SSO, but if that ever changes this value won't always be true
userDecryptionOption.TrustedDeviceOption = new TrustedDeviceUserDecryptionOption(hasAdminApproval);
userDecryptionOption.TrustedDeviceOption = new TrustedDeviceUserDecryptionOption(
hasAdminApproval,
hasLoginApprovingDevice,
hasManageResetPasswordPermission,
encryptedPrivateKey,
encryptedUserKey);
}
return userDecryptionOption;
}
private async Task<SsoConfigurationData?> GetSsoConfigurationDataAsync(ClaimsPrincipal subject)
private async Task<SsoConfig?> GetSsoConfigurationDataAsync(ClaimsPrincipal subject)
{
var organizationClaim = subject?.FindFirstValue("organizationId");
@ -629,6 +670,6 @@ public abstract class BaseRequestValidator<T> where T : class
return null;
}
return ssoConfig.GetData();
return ssoConfig;
}
}

View File

@ -0,0 +1,19 @@
using Bit.Core.Enums;
using Bit.Core.Utilities;
namespace Bit.Identity.Utilities;
public static class LoginApprovingDeviceTypes
{
private static readonly IReadOnlyCollection<DeviceType> _deviceTypes;
static LoginApprovingDeviceTypes()
{
var deviceTypes = new List<DeviceType>();
deviceTypes.AddRange(DeviceTypes.DesktopTypes);
deviceTypes.AddRange(DeviceTypes.MobileTypes);
_deviceTypes = deviceTypes.AsReadOnly();
}
public static IReadOnlyCollection<DeviceType> Types => _deviceTypes;
}