1
0
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:
Kyle Spearrin
2023-10-30 08:40:06 -05:00
committed by GitHub
parent 330e41a6d9
commit 44c559c723
33 changed files with 1207 additions and 5 deletions

View File

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

View File

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