Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-4167] Add PRF attestation flow during passkey registration #3339

2 changes: 1 addition & 1 deletion src/Api/Auth/Controllers/WebAuthnController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public async Task Post([FromBody] WebAuthnCredentialRequestModel model)
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);
var success = await _userService.CompleteWebAuthLoginRegistrationAsync(user, model.Name, tokenable.Options, model.DeviceResponse, model.SupportsPrf, model.EncryptedUserKey, model.EncryptedPublicKey, model.EncryptedPrivateKey);
if (!success)
{
throw new BadRequestException("Unable to complete WebAuthn registration.");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Utilities;
using Fido2NetLib;

namespace Bit.Api.Auth.Models.Request.Webauthn;
Expand All @@ -13,5 +14,20 @@ public class WebAuthnCredentialRequestModel

[Required]
public string Token { get; set; }

[Required]
public bool SupportsPrf { get; set; }

[EncryptedString]
[EncryptedStringLength(2000)]
public string EncryptedUserKey { get; set; }

[EncryptedString]
[EncryptedStringLength(2000)]
public string EncryptedPublicKey { get; set; }

[EncryptedString]
[EncryptedStringLength(2000)]
public string EncryptedPrivateKey { get; set; }
}

Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Models.Api;

namespace Bit.Api.Auth.Models.Response.WebAuthn;
Expand All @@ -11,10 +12,10 @@ public WebAuthnCredentialResponseModel(WebAuthnCredential credential) : base(Res
{
Id = credential.Id.ToString();
Name = credential.Name;
PrfSupport = false;
PrfStatus = credential.GetPrfStatus();
}

public string Id { get; set; }
public string Name { get; set; }
public bool PrfSupport { get; set; }
public WebAuthnPrfStatus PrfStatus { get; set; }
}
19 changes: 19 additions & 0 deletions src/Core/Auth/Entities/WebAuthnCredential.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Auth.Enums;
using Bit.Core.Entities;
using Bit.Core.Utilities;

Expand All @@ -18,8 +19,11 @@ public class WebAuthnCredential : ITableObject<Guid>
[MaxLength(20)]
public string Type { get; set; }
public Guid AaGuid { get; set; }
[MaxLength(2000)]
public string EncryptedUserKey { get; set; }
[MaxLength(2000)]
public string EncryptedPrivateKey { get; set; }
[MaxLength(2000)]
public string EncryptedPublicKey { get; set; }
public bool SupportsPrf { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
Expand All @@ -29,4 +33,19 @@ public void SetNewId()
{
Id = CoreHelpers.GenerateComb();
}

public WebAuthnPrfStatus GetPrfStatus()
{
if (!SupportsPrf)
{
return WebAuthnPrfStatus.Unsupported;
}

if (EncryptedUserKey != null && EncryptedPrivateKey != null && EncryptedPublicKey != null)
{
return WebAuthnPrfStatus.Enabled;
}

return WebAuthnPrfStatus.Supported;
}
}
8 changes: 8 additions & 0 deletions src/Core/Auth/Enums/WebAuthnPrfStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Bit.Core.Auth.Enums;

public enum WebAuthnPrfStatus
{
Enabled = 0,
Supported = 1,
Unsupported = 2
}
2 changes: 1 addition & 1 deletion src/Core/Services/IUserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public interface IUserService
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<bool> CompleteWebAuthLoginRegistrationAsync(User user, string name, CredentialCreateOptions options, AuthenticatorAttestationRawResponse attestationResponse, bool supportsPrf, string encryptedUserKey = null, string encryptedPublicKey = null, string encryptedPrivateKey = null);
Task<AssertionOptions> StartWebAuthnLoginAssertionAsync(User user);
Task<string> CompleteWebAuthLoginAssertionAsync(AuthenticatorAssertionRawResponse assertionResponse, User user);
Task SendEmailVerificationAsync(User user);
Expand Down
12 changes: 8 additions & 4 deletions src/Core/Services/Implementations/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -543,9 +543,9 @@ public async Task<CredentialCreateOptions> StartWebAuthnLoginRegistrationAsync(U
return options;
}

public async Task<bool> CompleteWebAuthLoginRegistrationAsync(User user, string name,
CredentialCreateOptions options,
AuthenticatorAttestationRawResponse attestationResponse)
public async Task<bool> CompleteWebAuthLoginRegistrationAsync(User user, string name, CredentialCreateOptions options,
AuthenticatorAttestationRawResponse attestationResponse, bool supportsPrf,
string encryptedUserKey = null, string encryptedPublicKey = null, string encryptedPrivateKey = null)
{
var existingCredentials = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id);
if (existingCredentials.Count >= 5)
Expand All @@ -566,7 +566,11 @@ public async Task<bool> CompleteWebAuthLoginRegistrationAsync(User user, string
Type = success.Result.CredType,
AaGuid = success.Result.Aaguid,
Counter = (int)success.Result.Counter,
UserId = user.Id
UserId = user.Id,
SupportsPrf = supportsPrf,
EncryptedUserKey = encryptedUserKey,
EncryptedPublicKey = encryptedPublicKey,
EncryptedPrivateKey = encryptedPrivateKey
};

await _webAuthnCredentialRepository.CreateAsync(credential);
Expand Down
10 changes: 5 additions & 5 deletions test/Api.Test/Auth/Controllers/WebAuthnControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class WebAuthnControllerTests
[Theory, BitAutoData]
public async Task Get_UserNotFound_ThrowsUnauthorizedAccessException(SutProvider<WebAuthnController> sutProvider)
{
// Arrange
// Arrange
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();

// Act
Expand All @@ -35,7 +35,7 @@ public async Task Get_UserNotFound_ThrowsUnauthorizedAccessException(SutProvider
[Theory, BitAutoData]
public async Task PostOptions_UserNotFound_ThrowsUnauthorizedAccessException(SecretVerificationRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
{
// Arrange
// Arrange
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();

// Act
Expand All @@ -62,7 +62,7 @@ public async Task PostOptions_UserVerificationFailed_ThrowsBadRequestException(S
[Theory, BitAutoData]
public async Task Post_UserNotFound_ThrowsUnauthorizedAccessException(WebAuthnCredentialRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
{
// Arrange
// Arrange
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();

// Act
Expand Down Expand Up @@ -100,7 +100,7 @@ public async Task Post_ValidInput_Returns(WebAuthnCredentialRequestModel request
.GetUserByPrincipalAsync(default)
.ReturnsForAnyArgs(user);
sutProvider.GetDependency<IUserService>()
.CompleteWebAuthLoginRegistrationAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>())
.CompleteWebAuthLoginRegistrationAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey)
.Returns(true);
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()
.Unprotect(requestModel.Token)
Expand All @@ -116,7 +116,7 @@ public async Task Post_ValidInput_Returns(WebAuthnCredentialRequestModel request
[Theory, BitAutoData]
public async Task Delete_UserNotFound_ThrowsUnauthorizedAccessException(Guid credentialId, SecretVerificationRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
{
// Arrange
// Arrange
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();

// Act
Expand Down
2 changes: 1 addition & 1 deletion test/Core.Test/Services/UserServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ public async void CompleteWebAuthLoginRegistrationAsync_ExceedsExistingCredentia
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(existingCredentials);

// Act
var result = await sutProvider.Sut.CompleteWebAuthLoginRegistrationAsync(user, "name", options, response);
var result = await sutProvider.Sut.CompleteWebAuthLoginRegistrationAsync(user, "name", options, response, false, null, null, null);

// Assert
Assert.False(result);
Expand Down
Loading