mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 08:02:49 -05:00
Support for passkey registration (#2885)
* support for fido2 auth * stub out registration implementations * stub out assertion steps and token issuance * verify token * webauthn tokenable * remove duplicate expiration set * revert sqlproj changes * update sqlproj target framework * update new validator signature * [PM-2014] Passkey registration (#2915) * [PM-2014] chore: rename `IWebAuthnRespository` to `IWebAuthnCredentialRepository` * [PM-2014] fix: add missing service registration * [PM-2014] feat: add user verification when fetching options * [PM-2014] feat: create migration script for mssql * [PM-2014] chore: append to todo comment * [PM-2014] feat: add support for creation token * [PM-2014] feat: implement credential saving * [PM-2014] chore: add resident key TODO comment * [PM-2014] feat: implement passkey listing * [PM-2014] feat: implement deletion without user verification * [PM-2014] feat: add user verification to delete * [PM-2014] feat: implement passkey limit * [PM-2014] chore: clean up todo comments * [PM-2014] fix: add missing sql scripts Missed staging them when commiting * [PM-2014] feat: include options response model in swagger docs * [PM-2014] chore: move properties after ctor * [PM-2014] feat: use `Guid` directly as input paramter * [PM-2014] feat: use nullable guid in token * [PM-2014] chore: add new-line * [PM-2014] feat: add support for feature flag * [PM-2014] feat: start adding controller tests * [PM-2014] feat: add user verification test * [PM-2014] feat: add controller tests for token interaction * [PM-2014] feat: add tokenable tests * [PM-2014] chore: clean up commented premium check * [PM-2014] feat: add user service test for credential limit * [PM-2014] fix: run `dotnet format` * [PM-2014] chore: remove trailing comma * [PM-2014] chore: add `Async` suffix * [PM-2014] chore: move delay to constant * [PM-2014] chore: change `default` to `null` * [PM-2014] chore: remove autogenerated weirdness * [PM-2014] fix: lint * Added check for PasswordlessLogin feature flag on new controller and methods. (#3284) * Added check for PasswordlessLogin feature flag on new controller and methods. * fix: build error from missing constructor argument --------- Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com> * [PM-4171] Update DB to support PRF (#3321) * [PM-4171] feat: update database to support PRF * [PM-4171] feat: rename `DescriptorId` to `CredentialId` * [PM-4171] feat: add PRF felds to domain object * [PM-4171] feat: add `SupportsPrf` column * [PM-4171] fix: add missing comma * [PM-4171] fix: add comma * [PM-3263] fix identity server tests for passkey registration (#3331) * Added WebAuthnRepo to EF DI * updated config to match current grant types * Remove ExtensionGrantValidator (#3363) * Linting --------- Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com> Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com> Co-authored-by: Ike <137194738+ike-kottlowski@users.noreply.github.com> Co-authored-by: Todd Martin <tmartin@bitwarden.com>
This commit is contained in:
@ -27,6 +27,10 @@ public interface IUserService
|
||||
Task<CredentialCreateOptions> StartWebAuthnRegistrationAsync(User user);
|
||||
Task<bool> DeleteWebAuthnKeyAsync(User user, int id);
|
||||
Task<bool> CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse);
|
||||
Task<CredentialCreateOptions> StartWebAuthnLoginRegistrationAsync(User user);
|
||||
Task<bool> CompleteWebAuthLoginRegistrationAsync(User user, string name, CredentialCreateOptions options, AuthenticatorAttestationRawResponse attestationResponse);
|
||||
Task<AssertionOptions> StartWebAuthnLoginAssertionAsync(User user);
|
||||
Task<string> CompleteWebAuthLoginAssertionAsync(AuthenticatorAssertionRawResponse assertionResponse, User user);
|
||||
Task SendEmailVerificationAsync(User user);
|
||||
Task<IdentityResult> ConfirmEmailAsync(User user, string token);
|
||||
Task InitiateEmailChangeAsync(User user, string newEmail);
|
||||
|
@ -1,8 +1,11 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -10,6 +13,7 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
@ -56,6 +60,8 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IStripeSyncService _stripeSyncService;
|
||||
private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository;
|
||||
private readonly IDataProtectorTokenFactory<WebAuthnLoginTokenable> _webAuthnLoginTokenizer;
|
||||
|
||||
public UserService(
|
||||
IUserRepository userRepository,
|
||||
@ -86,7 +92,9 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
IGlobalSettings globalSettings,
|
||||
IOrganizationService organizationService,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IStripeSyncService stripeSyncService)
|
||||
IStripeSyncService stripeSyncService,
|
||||
IWebAuthnCredentialRepository webAuthnRepository,
|
||||
IDataProtectorTokenFactory<WebAuthnLoginTokenable> webAuthnLoginTokenizer)
|
||||
: base(
|
||||
store,
|
||||
optionsAccessor,
|
||||
@ -123,6 +131,8 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
_organizationService = organizationService;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_stripeSyncService = stripeSyncService;
|
||||
_webAuthnCredentialRepository = webAuthnRepository;
|
||||
_webAuthnLoginTokenizer = webAuthnLoginTokenizer;
|
||||
}
|
||||
|
||||
public Guid? GetProperUserId(ClaimsPrincipal principal)
|
||||
@ -503,6 +513,125 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<CredentialCreateOptions> StartWebAuthnLoginRegistrationAsync(User user)
|
||||
{
|
||||
var fidoUser = new Fido2User
|
||||
{
|
||||
DisplayName = user.Name,
|
||||
Name = user.Email,
|
||||
Id = user.Id.ToByteArray(),
|
||||
};
|
||||
|
||||
// Get existing keys to exclude
|
||||
var existingKeys = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id);
|
||||
var excludeCredentials = existingKeys
|
||||
.Select(k => new PublicKeyCredentialDescriptor(CoreHelpers.Base64UrlDecode(k.CredentialId)))
|
||||
.ToList();
|
||||
|
||||
var authenticatorSelection = new AuthenticatorSelection
|
||||
{
|
||||
AuthenticatorAttachment = null,
|
||||
RequireResidentKey = false, // TODO: This is using the old residentKey selection variant, we need to update our lib so that we can set this to preferred
|
||||
UserVerification = UserVerificationRequirement.Preferred
|
||||
};
|
||||
|
||||
var extensions = new AuthenticationExtensionsClientInputs { };
|
||||
|
||||
var options = _fido2.RequestNewCredential(fidoUser, excludeCredentials, authenticatorSelection,
|
||||
AttestationConveyancePreference.None, extensions);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
public async Task<bool> CompleteWebAuthLoginRegistrationAsync(User user, string name,
|
||||
CredentialCreateOptions options,
|
||||
AuthenticatorAttestationRawResponse attestationResponse)
|
||||
{
|
||||
var existingCredentials = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id);
|
||||
if (existingCredentials.Count >= 5)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var existingCredentialIds = existingCredentials.Select(c => c.CredentialId);
|
||||
IsCredentialIdUniqueToUserAsyncDelegate callback = (args, cancellationToken) => Task.FromResult(!existingCredentialIds.Contains(CoreHelpers.Base64UrlEncode(args.CredentialId)));
|
||||
|
||||
var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback);
|
||||
|
||||
var credential = new WebAuthnCredential
|
||||
{
|
||||
Name = name,
|
||||
CredentialId = CoreHelpers.Base64UrlEncode(success.Result.CredentialId),
|
||||
PublicKey = CoreHelpers.Base64UrlEncode(success.Result.PublicKey),
|
||||
Type = success.Result.CredType,
|
||||
AaGuid = success.Result.Aaguid,
|
||||
Counter = (int)success.Result.Counter,
|
||||
UserId = user.Id
|
||||
};
|
||||
|
||||
await _webAuthnCredentialRepository.CreateAsync(credential);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<AssertionOptions> StartWebAuthnLoginAssertionAsync(User user)
|
||||
{
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
|
||||
var existingKeys = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id);
|
||||
var existingCredentials = existingKeys
|
||||
.Select(k => new PublicKeyCredentialDescriptor(CoreHelpers.Base64UrlDecode(k.CredentialId)))
|
||||
.ToList();
|
||||
|
||||
if (existingCredentials.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: PRF?
|
||||
var exts = new AuthenticationExtensionsClientInputs
|
||||
{
|
||||
UserVerificationMethod = true
|
||||
};
|
||||
var options = _fido2.GetAssertionOptions(existingCredentials, UserVerificationRequirement.Preferred, exts);
|
||||
|
||||
// TODO: temp save options to user record somehow
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
public async Task<string> CompleteWebAuthLoginAssertionAsync(AuthenticatorAssertionRawResponse assertionResponse, User user)
|
||||
{
|
||||
// TODO: Get options from user record somehow, then clear them
|
||||
var options = AssertionOptions.FromJson("");
|
||||
|
||||
var userCredentials = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id);
|
||||
var assertionId = CoreHelpers.Base64UrlEncode(assertionResponse.Id);
|
||||
var credential = userCredentials.FirstOrDefault(c => c.CredentialId == assertionId);
|
||||
if (credential == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: Callback to ensure credential ID is unique. Do we care? I don't think so.
|
||||
IsUserHandleOwnerOfCredentialIdAsync callback = (args, cancellationToken) => Task.FromResult(true);
|
||||
var credentialPublicKey = CoreHelpers.Base64UrlDecode(credential.PublicKey);
|
||||
var assertionVerificationResult = await _fido2.MakeAssertionAsync(
|
||||
assertionResponse, options, credentialPublicKey, (uint)credential.Counter, callback);
|
||||
|
||||
// Update SignatureCounter
|
||||
credential.Counter = (int)assertionVerificationResult.Counter;
|
||||
await _webAuthnCredentialRepository.ReplaceAsync(credential);
|
||||
|
||||
if (assertionVerificationResult.Status == "ok")
|
||||
{
|
||||
var token = _webAuthnLoginTokenizer.Protect(new WebAuthnLoginTokenable(user));
|
||||
return token;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendEmailVerificationAsync(User user)
|
||||
{
|
||||
if (user.EmailVerified)
|
||||
|
Reference in New Issue
Block a user