-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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>
- Loading branch information
1 parent
330e41a
commit 44c559c
Showing
33 changed files
with
1,207 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
using Bit.Api.Auth.Models.Request.Accounts; | ||
using Bit.Api.Auth.Models.Request.Webauthn; | ||
using Bit.Api.Auth.Models.Response.WebAuthn; | ||
using Bit.Api.Models.Response; | ||
using Bit.Core; | ||
using Bit.Core.Auth.Models.Business.Tokenables; | ||
using Bit.Core.Auth.Repositories; | ||
using Bit.Core.Exceptions; | ||
using Bit.Core.Services; | ||
using Bit.Core.Tokens; | ||
using Bit.Core.Utilities; | ||
using Microsoft.AspNetCore.Authorization; | ||
using Microsoft.AspNetCore.Mvc; | ||
|
||
namespace Bit.Api.Auth.Controllers; | ||
|
||
[Route("webauthn")] | ||
[Authorize("Web")] | ||
[RequireFeature(FeatureFlagKeys.PasswordlessLogin)] | ||
public class WebAuthnController : Controller | ||
{ | ||
private readonly IUserService _userService; | ||
private readonly IWebAuthnCredentialRepository _credentialRepository; | ||
private readonly IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> _createOptionsDataProtector; | ||
|
||
public WebAuthnController( | ||
IUserService userService, | ||
IWebAuthnCredentialRepository credentialRepository, | ||
IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> createOptionsDataProtector) | ||
{ | ||
_userService = userService; | ||
_credentialRepository = credentialRepository; | ||
_createOptionsDataProtector = createOptionsDataProtector; | ||
} | ||
|
||
[HttpGet("")] | ||
public async Task<ListResponseModel<WebAuthnCredentialResponseModel>> Get() | ||
{ | ||
var user = await GetUserAsync(); | ||
var credentials = await _credentialRepository.GetManyByUserIdAsync(user.Id); | ||
|
||
return new ListResponseModel<WebAuthnCredentialResponseModel>(credentials.Select(c => new WebAuthnCredentialResponseModel(c))); | ||
} | ||
|
||
[HttpPost("options")] | ||
public async Task<WebAuthnCredentialCreateOptionsResponseModel> PostOptions([FromBody] SecretVerificationRequestModel model) | ||
{ | ||
var user = await VerifyUserAsync(model); | ||
var options = await _userService.StartWebAuthnLoginRegistrationAsync(user); | ||
|
||
var tokenable = new WebAuthnCredentialCreateOptionsTokenable(user, options); | ||
var token = _createOptionsDataProtector.Protect(tokenable); | ||
|
||
return new WebAuthnCredentialCreateOptionsResponseModel | ||
{ | ||
Options = options, | ||
Token = token | ||
}; | ||
} | ||
|
||
[HttpPost("")] | ||
public async Task Post([FromBody] WebAuthnCredentialRequestModel model) | ||
{ | ||
var user = await GetUserAsync(); | ||
var tokenable = _createOptionsDataProtector.Unprotect(model.Token); | ||
if (!tokenable.TokenIsValid(user)) | ||
{ | ||
throw new BadRequestException("The token associated with your request is expired. A valid token is required to continue."); | ||
} | ||
|
||
var success = await _userService.CompleteWebAuthLoginRegistrationAsync(user, model.Name, tokenable.Options, model.DeviceResponse); | ||
if (!success) | ||
{ | ||
throw new BadRequestException("Unable to complete WebAuthn registration."); | ||
} | ||
} | ||
|
||
[HttpPost("{id}/delete")] | ||
public async Task Delete(Guid id, [FromBody] SecretVerificationRequestModel model) | ||
{ | ||
var user = await VerifyUserAsync(model); | ||
var credential = await _credentialRepository.GetByIdAsync(id, user.Id); | ||
if (credential == null) | ||
{ | ||
throw new NotFoundException("Credential not found."); | ||
} | ||
|
||
await _credentialRepository.DeleteAsync(credential); | ||
} | ||
|
||
private async Task<Core.Entities.User> GetUserAsync() | ||
{ | ||
var user = await _userService.GetUserByPrincipalAsync(User); | ||
if (user == null) | ||
{ | ||
throw new UnauthorizedAccessException(); | ||
} | ||
return user; | ||
} | ||
|
||
private async Task<Core.Entities.User> VerifyUserAsync(SecretVerificationRequestModel model) | ||
{ | ||
var user = await GetUserAsync(); | ||
if (!await _userService.VerifySecretAsync(user, model.Secret)) | ||
{ | ||
await Task.Delay(Constants.FailedSecretVerificationDelay); | ||
throw new BadRequestException(string.Empty, "User verification failed."); | ||
} | ||
|
||
return user; | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
src/Api/Auth/Models/Request/WebAuthn/WebAuthnCredentialRequestModel.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
using System.ComponentModel.DataAnnotations; | ||
using Fido2NetLib; | ||
|
||
namespace Bit.Api.Auth.Models.Request.Webauthn; | ||
|
||
public class WebAuthnCredentialRequestModel | ||
{ | ||
[Required] | ||
public AuthenticatorAttestationRawResponse DeviceResponse { get; set; } | ||
|
||
[Required] | ||
public string Name { get; set; } | ||
|
||
[Required] | ||
public string Token { get; set; } | ||
} | ||
|
16 changes: 16 additions & 0 deletions
16
src/Api/Auth/Models/Response/WebAuthn/WebAuthnCredentialCreateOptionsResponseModel.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
using Bit.Core.Models.Api; | ||
using Fido2NetLib; | ||
|
||
namespace Bit.Api.Auth.Models.Response.WebAuthn; | ||
|
||
public class WebAuthnCredentialCreateOptionsResponseModel : ResponseModel | ||
{ | ||
private const string ResponseObj = "webauthnCredentialCreateOptions"; | ||
|
||
public WebAuthnCredentialCreateOptionsResponseModel() : base(ResponseObj) | ||
{ | ||
} | ||
|
||
public CredentialCreateOptions Options { get; set; } | ||
public string Token { get; set; } | ||
} |
20 changes: 20 additions & 0 deletions
20
src/Api/Auth/Models/Response/WebAuthn/WebAuthnCredentialResponseModel.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
using Bit.Core.Auth.Entities; | ||
using Bit.Core.Models.Api; | ||
|
||
namespace Bit.Api.Auth.Models.Response.WebAuthn; | ||
|
||
public class WebAuthnCredentialResponseModel : ResponseModel | ||
{ | ||
private const string ResponseObj = "webauthnCredential"; | ||
|
||
public WebAuthnCredentialResponseModel(WebAuthnCredential credential) : base(ResponseObj) | ||
{ | ||
Id = credential.Id.ToString(); | ||
Name = credential.Name; | ||
PrfSupport = false; | ||
} | ||
|
||
public string Id { get; set; } | ||
public string Name { get; set; } | ||
public bool PrfSupport { get; set; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
using System.ComponentModel.DataAnnotations; | ||
using Bit.Core.Entities; | ||
using Bit.Core.Utilities; | ||
|
||
namespace Bit.Core.Auth.Entities; | ||
|
||
public class WebAuthnCredential : ITableObject<Guid> | ||
{ | ||
public Guid Id { get; set; } | ||
public Guid UserId { get; set; } | ||
[MaxLength(50)] | ||
public string Name { get; set; } | ||
[MaxLength(256)] | ||
public string PublicKey { get; set; } | ||
[MaxLength(256)] | ||
public string CredentialId { get; set; } | ||
public int Counter { get; set; } | ||
[MaxLength(20)] | ||
public string Type { get; set; } | ||
public Guid AaGuid { get; set; } | ||
public string EncryptedUserKey { get; set; } | ||
public string EncryptedPrivateKey { get; set; } | ||
public string EncryptedPublicKey { get; set; } | ||
public bool SupportsPrf { get; set; } | ||
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; | ||
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; | ||
|
||
public void SetNewId() | ||
{ | ||
Id = CoreHelpers.GenerateComb(); | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
src/Core/Auth/Models/Business/Tokenables/WebAuthnCredentialCreateOptionsTokenable.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
using System.Text.Json.Serialization; | ||
using Bit.Core.Entities; | ||
using Bit.Core.Tokens; | ||
using Fido2NetLib; | ||
|
||
namespace Bit.Core.Auth.Models.Business.Tokenables; | ||
|
||
public class WebAuthnCredentialCreateOptionsTokenable : ExpiringTokenable | ||
{ | ||
// 7 minutes = max webauthn timeout (6 minutes) + slack for miscellaneous delays | ||
private const double _tokenLifetimeInHours = (double)7 / 60; | ||
public const string ClearTextPrefix = "BWWebAuthnCredentialCreateOptions_"; | ||
public const string DataProtectorPurpose = "WebAuthnCredentialCreateDataProtector"; | ||
public const string TokenIdentifier = "WebAuthnCredentialCreateOptionsToken"; | ||
|
||
public string Identifier { get; set; } = TokenIdentifier; | ||
public Guid? UserId { get; set; } | ||
public CredentialCreateOptions Options { get; set; } | ||
|
||
[JsonConstructor] | ||
public WebAuthnCredentialCreateOptionsTokenable() | ||
{ | ||
ExpirationDate = DateTime.UtcNow.AddHours(_tokenLifetimeInHours); | ||
} | ||
|
||
public WebAuthnCredentialCreateOptionsTokenable(User user, CredentialCreateOptions options) : this() | ||
{ | ||
UserId = user?.Id; | ||
Options = options; | ||
} | ||
|
||
public bool TokenIsValid(User user) | ||
{ | ||
if (!Valid || user == null) | ||
{ | ||
return false; | ||
} | ||
|
||
return UserId == user.Id; | ||
} | ||
|
||
protected override bool TokenIsValid() => Identifier == TokenIdentifier && UserId != null && Options != null; | ||
} | ||
|
43 changes: 43 additions & 0 deletions
43
src/Core/Auth/Models/Business/Tokenables/WebAuthnLoginTokenable.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
using System.Text.Json.Serialization; | ||
using Bit.Core.Entities; | ||
using Bit.Core.Tokens; | ||
|
||
namespace Bit.Core.Auth.Models.Business.Tokenables; | ||
|
||
public class WebAuthnLoginTokenable : ExpiringTokenable | ||
{ | ||
private const double _tokenLifetimeInHours = (double)1 / 60; // 1 minute | ||
public const string ClearTextPrefix = "BWWebAuthnLogin_"; | ||
public const string DataProtectorPurpose = "WebAuthnLoginDataProtector"; | ||
public const string TokenIdentifier = "WebAuthnLoginToken"; | ||
|
||
public string Identifier { get; set; } = TokenIdentifier; | ||
public Guid Id { get; set; } | ||
public string Email { get; set; } | ||
|
||
[JsonConstructor] | ||
public WebAuthnLoginTokenable() | ||
{ | ||
ExpirationDate = DateTime.UtcNow.AddHours(_tokenLifetimeInHours); | ||
} | ||
|
||
public WebAuthnLoginTokenable(User user) : this() | ||
{ | ||
Id = user?.Id ?? default; | ||
Email = user?.Email; | ||
} | ||
|
||
public bool TokenIsValid(User user) | ||
{ | ||
if (Id == default || Email == default || user == null) | ||
{ | ||
return false; | ||
} | ||
|
||
return Id == user.Id && | ||
Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase); | ||
} | ||
|
||
// Validates deserialized | ||
protected override bool TokenIsValid() => Identifier == TokenIdentifier && Id != default && !string.IsNullOrWhiteSpace(Email); | ||
} |
10 changes: 10 additions & 0 deletions
10
src/Core/Auth/Repositories/IWebAuthnCredentialRepository.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
using Bit.Core.Auth.Entities; | ||
using Bit.Core.Repositories; | ||
|
||
namespace Bit.Core.Auth.Repositories; | ||
|
||
public interface IWebAuthnCredentialRepository : IRepository<WebAuthnCredential, Guid> | ||
{ | ||
Task<WebAuthnCredential> GetByIdAsync(Guid id, Guid userId); | ||
Task<ICollection<WebAuthnCredential>> GetManyByUserIdAsync(Guid userId); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.