Skip to content

Pm 2023 fido2 authentication #73

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

Open
wants to merge 57 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
d91e22e
support for fido2 auth
kspearrin Apr 26, 2023
9dc24de
stub out registration implementations
kspearrin Apr 28, 2023
f33570e
stub out assertion steps and token issuance
kspearrin May 1, 2023
bd01206
verify token
kspearrin May 1, 2023
0abba03
webauthn tokenable
kspearrin May 1, 2023
6d4aeff
remove duplicate expiration set
kspearrin May 1, 2023
74e56ee
[PM-2014] chore: rename `IWebAuthnRespository` to `IWebAuthnCredentia…
coroiu May 8, 2023
cdf531d
[PM-2014] fix: add missing service registration
coroiu May 8, 2023
00d92ed
[PM-2014] feat: add user verification when fetching options
coroiu May 8, 2023
f8fd97e
[PM-2014] feat: create migration script for mssql
coroiu May 8, 2023
f8ee4fa
[PM-2014] chore: append to todo comment
coroiu May 8, 2023
ebec5cb
[PM-2014] feat: add support for creation token
coroiu May 9, 2023
88dce90
[PM-2014] feat: implement credential saving
coroiu May 9, 2023
556b758
[PM-2014] chore: add resident key TODO comment
coroiu May 9, 2023
ee32d5a
[PM-2014] feat: implement passkey listing
coroiu May 10, 2023
73b7a00
[PM-2014] feat: implement deletion without user verification
coroiu May 10, 2023
d63588a
Merge branch 'master' into fido2
kspearrin May 10, 2023
f2c6b03
revert sqlproj changes
kspearrin May 10, 2023
26da1b2
update sqlproj target framework
kspearrin May 10, 2023
ec0c93f
update new validator signature
kspearrin May 10, 2023
258afae
[PM-2014] feat: add user verification to delete
coroiu May 11, 2023
6aff50a
[PM-2014] feat: implement passkey limit
coroiu May 11, 2023
e8ab5d7
[PM-2014] chore: clean up todo comments
coroiu May 11, 2023
85db96c
[PM-2014] fix: add missing sql scripts
coroiu May 11, 2023
934dd82
[PM-2014] feat: include options response model in swagger docs
coroiu May 15, 2023
74d8f2e
[PM-2014] chore: move properties after ctor
coroiu May 15, 2023
40e56ef
[PM-2014] feat: use `Guid` directly as input paramter
coroiu May 15, 2023
e718989
[PM-2014] feat: use nullable guid in token
coroiu May 15, 2023
f17ea48
[PM-2014] chore: add new-line
coroiu May 15, 2023
59704b9
Merge branch 'fido2' into PM-2014-passkey-registration
coroiu May 15, 2023
75edd13
[PM-2014] feat: add support for feature flag
coroiu May 15, 2023
c7ee102
[PM-2014] feat: start adding controller tests
coroiu May 15, 2023
590713f
[PM-2014] feat: add user verification test
coroiu May 15, 2023
0a9544c
[PM-2014] feat: add controller tests for token interaction
coroiu May 15, 2023
204a63f
[PM-2014] feat: add tokenable tests
coroiu May 15, 2023
97048ea
[PM-2014] chore: clean up commented premium check
coroiu May 16, 2023
84cd8c7
[PM-2014] feat: add user service test for credential limit
coroiu May 16, 2023
158b20d
[PM-2014] fix: run `dotnet format`
coroiu May 16, 2023
1205e9f
[PM-2014] chore: remove trailing comma
coroiu May 17, 2023
2f64464
[PM-2014] chore: add `Async` suffix
coroiu May 17, 2023
91ff3e5
[PM-2014] chore: move delay to constant
coroiu May 17, 2023
edc4840
[PM-2014] chore: change `default` to `null`
coroiu May 17, 2023
c33249d
[PM-2014] chore: remove autogenerated weirdness
coroiu May 17, 2023
0233685
[PM-2241] feat: add prf support to database
coroiu May 17, 2023
94073fa
Merge branch 'master' into fido2
kspearrin May 17, 2023
1f1cbd5
[PM-2241] feat: add support for saving prf
coroiu May 17, 2023
7d3582d
[PM-2023] feat: implement assert options fetching
coroiu May 18, 2023
8754e0a
[PM-2023] feat: add support for assertion token
coroiu May 22, 2023
4e8fd85
[PM-2023] feat: implement working assertion
coroiu May 22, 2023
9b1ac4a
[PM-2023] feat: return prf keys to allow unlocking
coroiu May 22, 2023
5d11d06
[PM-2023] feat: webauthn login with email
coroiu May 23, 2023
07683f1
Merge branch 'master' into fido2
kspearrin May 25, 2023
cd5635f
[PM-2014] Passkey registration (#2915)
coroiu May 26, 2023
e3fcc5e
Merge branch 'master' into fido2
kspearrin Jun 1, 2023
38f48e2
Merge branch 'master' into fido2
coroiu Aug 24, 2023
3c7d2cc
Merge branch 'fido2' into pm-2023-fido2-authentication
coroiu Aug 24, 2023
7b358ad
[PM-2023] fix: linting
coroiu Aug 24, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions src/Api/Auth/Controllers/WebAuthnController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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 Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Bit.Api.Auth.Controllers;

[Route("webauthn")]
[Authorize("Web")]
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, model.UserKey, model.PrfPublicKey, model.PrfPrivateKey, model.SupportsPrf, 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.");
}
Comment on lines +102 to +106
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Potential timing attack vulnerability. Consider using a constant-time comparison


return user;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
using Fido2NetLib;

namespace Bit.Api.Auth.Models.Request.Webauthn;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider using 'WebAuthn' instead of 'Webauthn' in the namespace for consistency with the class name


public class WebAuthnCredentialRequestModel
{
[Required]
public AuthenticatorAttestationRawResponse DeviceResponse { get; set; }

[Required]
public string Name { get; set; }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Add length constraint to Name property


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

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

public string UserKey { get; set; }
public string PrfPublicKey { get; set; }
public string PrfPrivateKey { get; set; }
}

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider adding XML documentation comments to describe the purpose and usage of this class

{
private const string ResponseObj = "webauthnCredentialCreateOptions";

public WebAuthnCredentialCreateOptionsResponseModel() : base(ResponseObj)
{
}

public CredentialCreateOptions Options { get; set; }
public string Token { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
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();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider using Guid.ToString("N") for a more compact string representation without hyphens

Name = credential.Name;
PrfStatus = credential.GetPrfStatus();
}

public string Id { get; set; }
public string Name { get; set; }
public WebAuthnPrfStatus PrfStatus { get; set; }
}
48 changes: 48 additions & 0 deletions src/Core/Auth/Entities/WebAuthnCredential.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Auth.Enums;
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 DescriptorId { get; set; }
public int Counter { get; set; }
[MaxLength(20)]
public string Type { get; set; }
public Guid AaGuid { get; set; }
public string UserKey { get; set; }
public string PrfPublicKey { get; set; }
public string PrfPrivateKey { get; set; }
Comment on lines +22 to +24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: UserKey, PrfPublicKey, and PrfPrivateKey should likely have [MaxLength] attributes

public bool SupportsPrf { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
Comment on lines +26 to +27
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider using DateTimeOffset instead of DateTime for better timezone handling


public void SetNewId()
{
Id = CoreHelpers.GenerateComb();
}

public WebAuthnPrfStatus GetPrfStatus()
{
if (SupportsPrf && PrfPublicKey != null && PrfPrivateKey != null)
{
return WebAuthnPrfStatus.Enabled;
}
else if (SupportsPrf)
{
return WebAuthnPrfStatus.Supported;
}

return WebAuthnPrfStatus.Unsupported;
}
Comment on lines +34 to +46
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: GetPrfStatus method could be simplified using a switch expression


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

public enum WebAuthnPrfStatus
{
Enabled = 0,
Supported = 1,
Unsupported = 2
}
Comment on lines +3 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: consider adding XML documentation comments to describe the purpose of each enum value


Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;

namespace Bit.Core.Auth.Models.Api.Request.Accounts;

public class WebauthnCredentialAssertionOptionsRequestModel
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Class name inconsistent with file name (Webauthn vs WebAuthn)

{
[EmailAddress]
[StringLength(256)]
public string Email { get; set; }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Email property should be nullable (string?) for C# 8.0+ nullable reference types compatibility

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
using Fido2NetLib;

namespace Bit.Core.Auth.Models.Api.Request.Accounts;

public class WebauthnCredentialAssertionRequestModel
{
[Required]
public AuthenticatorAssertionRawResponse DeviceResponse { get; set; }

[Required]
public string Token { get; set; }
}
Comment on lines +6 to +13
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider adding XML documentation comments to describe the purpose of this class and its properties


Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Bit.Core.Models.Api;
using Fido2NetLib;

namespace Bit.Core.Auth.Models.Api.Response.Accounts;

public class WebAuthnCredentialAssertionOptionsResponseModel : ResponseModel
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Class could benefit from XML documentation for better API understanding

{
private const string ResponseObj = "webauthnCredentialAssertionOptions";

public WebAuthnCredentialAssertionOptionsResponseModel() : base(ResponseObj)
{
}

public AssertionOptions Options { get; set; }
public string Token { get; set; }
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Bit.Core.Models.Api;

namespace Bit.Core.Auth.Models.Api.Response.Accounts;

public class WebAuthnCredentialAssertionResponseModel : ResponseModel
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider adding XML documentation for the class

{
private const string ResponseObj = "webauthnCredentialAssertion";

public WebAuthnCredentialAssertionResponseModel() : base(ResponseObj)
{
}

public string Token { get; set; }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Add XML documentation for the Token property

}

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 WebAuthnCredentialAssertionOptionsTokenable : ExpiringTokenable
{
// 7 minutes = max webauthn timeout (6 minutes) + slack for miscellaneous delays
private const double _tokenLifetimeInHours = (double)7 / 60;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider using TimeSpan.FromMinutes(7) for better readability

public const string ClearTextPrefix = "BWWebAuthnCredentialAssertionOptions_";
public const string DataProtectorPurpose = "WebAuthnCredentialAssertionDataProtector";
public const string TokenIdentifier = "WebAuthnCredentialAssertionOptionsToken";

public string Identifier { get; set; } = TokenIdentifier;
public Guid? UserId { get; set; }
public AssertionOptions Options { get; set; }

[JsonConstructor]
public WebAuthnCredentialAssertionOptionsTokenable()
{
ExpirationDate = DateTime.UtcNow.AddHours(_tokenLifetimeInHours);
}

public WebAuthnCredentialAssertionOptionsTokenable(User user, AssertionOptions options) : this()
{
UserId = user?.Id;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Potential null reference exception if user is null

Options = options;
}

public bool TokenIsValid(User user)
{
if (!Valid || user == null)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider separating the null check for user to improve clarity

{
return false;
}

return UserId == user.Id;
}

protected override bool TokenIsValid() => Identifier == TokenIdentifier && UserId != null && Options != null;
}

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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Use DateTimeOffset.UtcNow instead of DateTime.UtcNow for better timezone handling

}

public WebAuthnCredentialCreateOptionsTokenable(User user, CredentialCreateOptions options) : this()
{
UserId = user?.Id;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Potential null reference exception if user is null

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;
}

Loading