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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user