1
0
mirror of https://github.com/bitwarden/server synced 2025-12-26 13:13:24 +00:00

[PM-2032] Server endpoints to support authentication with a passkey (#3361)

* [PM-2032] feat: add assertion options tokenable

* [PM-2032] feat: add request and response models

* [PM-2032] feat: implement `assertion-options` identity endpoint

* [PM-2032] feat: implement authentication with passkey

* [PM-2032] chore: rename to `WebAuthnGrantValidator`

* [PM-2032] fix: add missing subsitute

* [PM-2032] feat: start adding builder

* [PM-2032] feat: add support for KeyConnector

* [PM-2032] feat: add first version of TDE

* [PM-2032] chore: refactor WithSso

* [PM-2023] feat: add support for TDE feature flag

* [PM-2023] feat: add support for approving devices

* [PM-2023] feat: add support for hasManageResetPasswordPermission

* [PM-2032] feat: add support for hasAdminApproval

* [PM-2032] chore: don't supply device if not necessary

* [PM-2032] chore: clean up imports

* [PM-2023] feat: extract interface

* [PM-2023] chore: add clarifying comment

* [PM-2023] feat: use new builder in production code

* [PM-2032] feat: add support for PRF

* [PM-2032] chore: clean-up todos

* [PM-2023] chore: remove token which is no longer used

* [PM-2032] chore: remove todo

* [PM-2032] feat: improve assertion error handling

* [PM-2032] fix: linting issues

* [PM-2032] fix: revert changes to `launchSettings.json`

* [PM-2023] chore: clean up assertion endpoint

* [PM-2032] feat: bypass 2FA

* [PM-2032] fix: rename prf option to singular

* [PM-2032] fix: lint

* [PM-2032] fix: typo

* [PM-2032] chore: improve builder tests

Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>

* [PM-2032] chore: clarify why we don't require 2FA

* [PM-2023] feat: move `identityProvider` constant to common class

* [PM-2032] fix: lint

* [PM-2023] fix: move `IdentityProvider` to core.Constants

* [PM-2032] fix: missing import

* [PM-2032] chore: refactor token timespan to use `TimeSpan`

* [PM-2032] chore: make `StartWebAuthnLoginAssertion` sync

* [PM-2032] chore: use `FromMinutes`

* [PM-2032] fix: change to 17 minutes to cover webauthn assertion

* [PM-2032] chore: do not use `async void`

* [PM-2032] fix: comment saying wrong amount of minutes

* [PM-2032] feat: put validator behind feature flag

* [PM-2032] fix: lint

---------

Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
This commit is contained in:
Andreas Coroiu
2023-11-20 15:55:31 +01:00
committed by GitHub
parent 07c202ecaf
commit 80740aa4ba
24 changed files with 855 additions and 223 deletions

View File

@@ -10,7 +10,6 @@ using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Api.Response;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Utilities;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
@@ -23,7 +22,6 @@ 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;
using Microsoft.Extensions.Caching.Distributed;
@@ -35,7 +33,6 @@ public abstract class BaseRequestValidator<T> where T : class
private UserManager<User> _userManager;
private readonly IDeviceRepository _deviceRepository;
private readonly IDeviceService _deviceService;
private readonly IUserService _userService;
private readonly IEventService _eventService;
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider;
private readonly IOrganizationRepository _organizationRepository;
@@ -53,6 +50,8 @@ public abstract class BaseRequestValidator<T> where T : class
protected IPolicyService PolicyService { get; }
protected IFeatureService FeatureService { get; }
protected ISsoConfigRepository SsoConfigRepository { get; }
protected IUserService _userService { get; }
protected IUserDecryptionOptionsBuilder UserDecryptionOptionsBuilder { get; }
public BaseRequestValidator(
UserManager<User> userManager,
@@ -73,7 +72,8 @@ public abstract class BaseRequestValidator<T> where T : class
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
IFeatureService featureService,
ISsoConfigRepository ssoConfigRepository,
IDistributedCache distributedCache)
IDistributedCache distributedCache,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
{
_userManager = userManager;
_deviceRepository = deviceRepository;
@@ -96,11 +96,12 @@ public abstract class BaseRequestValidator<T> where T : class
_distributedCache = distributedCache;
_cacheEntryOptions = new DistributedCacheEntryOptions
{
// This sets the time an item is cached to 15 minutes. This value is hard coded
// to 15 because to it covers all time-out windows for both Authenticators and
// This sets the time an item is cached to 17 minutes. This value is hard coded
// to 17 because to it covers all time-out windows for both Authenticators and
// Email TOTP.
AbsoluteExpirationRelativeToNow = new TimeSpan(0, 15, 0)
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(17)
};
UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder;
}
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
@@ -333,7 +334,7 @@ public abstract class BaseRequestValidator<T> where T : class
protected abstract void SetErrorResult(T context, Dictionary<string, object> customResponse);
protected abstract ClaimsPrincipal GetSubject(T context);
private async Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request)
protected virtual async Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request)
{
if (request.GrantType == "client_credentials")
{
@@ -612,67 +613,12 @@ public abstract class BaseRequestValidator<T> where T : class
/// </summary>
private async Task<UserDecryptionOptions> CreateUserDecryptionOptionsAsync(User user, Device device, ClaimsPrincipal 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
userDecryptionOption.KeyConnectorOption = new KeyConnectorUserDecryptionOption(ssoConfigurationData.KeyConnectorUrl);
return userDecryptionOption;
}
// Only add the trusted device specific option when the flag is turned on
if (FeatureService.IsEnabled(FeatureFlagKeys.TrustedDeviceEncryption, CurrentContext) && ssoConfigurationData is { MemberDecryptionType: MemberDecryptionType.TrustedDeviceEncryption })
{
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,
hasLoginApprovingDevice,
hasManageResetPasswordPermission,
encryptedPrivateKey,
encryptedUserKey);
}
return userDecryptionOption;
var ssoConfig = await GetSsoConfigurationDataAsync(subject);
return await UserDecryptionOptionsBuilder
.ForUser(user)
.WithDevice(device)
.WithSso(ssoConfig)
.BuildAsync();
}
private async Task<SsoConfig?> GetSsoConfigurationDataAsync(ClaimsPrincipal subject)