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 2032 troubleshoot actions #65

Open
wants to merge 56 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 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
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
94073fa
Merge branch 'master' into fido2
kspearrin May 17, 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
155e5d0
Merge branch 'master' into fido2
trmartin4 Sep 20, 2023
84f7fc8
Added check for PasswordlessLogin feature flag on new controller and …
trmartin4 Sep 21, 2023
d11fc46
[PM-4171] Update DB to support PRF (#3321)
coroiu Oct 10, 2023
3bda447
[PM-3263] fix identity server tests for passkey registration (#3331)
ike-kottlowski Oct 12, 2023
8101435
[PM-4167] feat: add support for `SupportsPrf`
coroiu Oct 4, 2023
9580f7b
[PM-4167] feat: add `prfStatus` property
coroiu Oct 5, 2023
06b223c
[PM-4167] feat: add support for storing PRF keys
coroiu Oct 6, 2023
eae9fa6
[PM-4167] fix: allow credentials to be created without encryption sup…
coroiu Oct 12, 2023
72420d0
[PM-4167] fix: broken test
coroiu Oct 12, 2023
97cfd1e
[PM-4167] chore: remove whitespace
coroiu Oct 12, 2023
083b342
[PM-4167] fix: controller test
coroiu Oct 13, 2023
69af190
[PM-3936] [PM-4174] feat: update `UserVerificationRequirement` and `r…
coroiu Oct 12, 2023
120b182
[PM-3936] fix: lint
coroiu Oct 13, 2023
a28ea69
[PM-2032] feat: add assertion options tokenable
coroiu Oct 17, 2023
1ffd97e
[PM-2032] feat: add request and response models
coroiu Oct 18, 2023
0f44dda
[PM-2032] feat: implement `assertion-options` identity endpoint
coroiu Oct 18, 2023
3a738e3
Add LaunchDarkly flag override file to .gitignore (#3357)
coroiu Oct 18, 2023
70533a2
[PM-2032] feat: implement authentication with passkey
coroiu Oct 19, 2023
4f2ec30
[PM-2032] chore: rename to `WebAuthnGrantValidator`
coroiu Oct 19, 2023
17ff52d
[PM-2032] fix: add missing subsitute
coroiu Oct 27, 2023
15a5e06
[PM-2032] feat: start adding builder
coroiu Oct 27, 2023
367f58d
[PM-2032] feat: add support for KeyConnector
coroiu Oct 27, 2023
f601312
[PM-2032] feat: add first version of TDE
coroiu Oct 27, 2023
e79e295
[PM-2032] chore: refactor WithSso
coroiu Oct 27, 2023
ec6e30a
[PM-2023] feat: add support for TDE feature flag
coroiu Oct 27, 2023
6c04eee
[PM-2023] feat: add support for approving devices
coroiu Oct 27, 2023
fa0bdf6
[PM-2023] feat: add support for hasManageResetPasswordPermission
coroiu Oct 27, 2023
a38f719
[PM-2032] feat: add support for hasAdminApproval
coroiu Oct 27, 2023
f607774
[PM-2032] chore: don't supply device if not necessary
coroiu Oct 27, 2023
1612e24
[PM-2032] chore: clean up imports
coroiu Oct 27, 2023
673f9fd
[PM-2023] feat: extract interface
coroiu Oct 27, 2023
ed4b85c
[PM-2023] chore: add clarifying comment
coroiu Oct 27, 2023
03bb729
[PM-2023] feat: use new builder in production code
coroiu Oct 27, 2023
d62937a
[PM-2032] feat: add support for PRF
coroiu Oct 30, 2023
cdabd17
[PM-2032] chore: clean-up todos
coroiu Oct 30, 2023
5c4b14f
[PM-2023] chore: remove token which is no longer used
coroiu Oct 30, 2023
c5aedc9
[PM-2032] chore: remove todo
coroiu Oct 30, 2023
9cb7b57
[PM-2032] feat: improve assertion error handling
coroiu Oct 30, 2023
6f7efaa
[PM-2032] fix: linting issues
coroiu Oct 30, 2023
ca10935
[PM-2032] fix: revert changes to `launchSettings.json`
coroiu Oct 30, 2023
8087dda
[PM-2023] chore: clean up assertion endpoint
coroiu Oct 30, 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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,4 @@ src/Identity/Identity.zip
src/Notifications/Notifications.zip
bitwarden_license/src/Portal/Portal.zip
bitwarden_license/src/Sso/Sso.zip
src/Api/flags.json
**/src/*/flags.json
112 changes: 112 additions & 0 deletions src/Api/Auth/Controllers/WebAuthnController.cs
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)));
}
Comment on lines +36 to +43
Copy link

Choose a reason for hiding this comment

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

logic: Add authorization check to ensure the user can only access their own credentials


[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.SupportsPrf, model.EncryptedUserKey, model.EncryptedPublicKey, model.EncryptedPrivateKey, 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);
Copy link

Choose a reason for hiding this comment

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

logic: Add error handling for the DeleteAsync operation

}

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);
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 for secret verification

throw new BadRequestException(string.Empty, "User verification failed.");
}

return user;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Utilities;
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: Namespace 'Webauthn' is inconsistent with C# naming conventions. Consider changing to 'WebAuthn'.


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

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

[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
@@ -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 of this class and its properties

{
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; }
Comment on lines +18 to +20
Copy link

Choose a reason for hiding this comment

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

style: Properties should be read-only if they're only set in the constructor

}
50 changes: 50 additions & 0 deletions src/Core/Auth/Entities/WebAuthnCredential.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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>
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 to explain its purpose and usage

{
public Guid Id { get; set; }
public Guid UserId { get; set; }
[MaxLength(50)]
public string Name { get; set; }
Comment on lines +12 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 making Name property required using [Required] attribute

[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; }
[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; }
Copy link

Choose a reason for hiding this comment

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

logic: SupportsPrf should be initialized to false by default

public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;

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

public WebAuthnPrfStatus GetPrfStatus()
{
if (SupportsPrf && EncryptedUserKey != null && EncryptedPrivateKey != null && EncryptedPublicKey != null)
Copy link

Choose a reason for hiding this comment

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

style: Use null-coalescing operator (??) instead of null checks for better readability

{
return WebAuthnPrfStatus.Enabled;
}
else if (SupportsPrf)
{
return WebAuthnPrfStatus.Supported;
}

return WebAuthnPrfStatus.Unsupported;
}
Comment on lines +37 to +49
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 pattern matching

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

public enum WebAuthnLoginAssertionOptionsScope
{
Authentication = 0,
PrfRegistration = 1
}
Comment on lines +3 to +7
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 enum and its values

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
Comment on lines +5 to +7
Copy link

Choose a reason for hiding this comment

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

logic: Verify that the order of these enum values aligns with the intended logic in the WebAuthn implementation

}
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 this enum and each of its values

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

Copy link

Choose a reason for hiding this comment

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

style: Remove this empty line at the beginning of the file.

using Bit.Core.Models.Api;
using Fido2NetLib;

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

public class WebAuthnLoginAssertionOptionsResponseModel : ResponseModel
{
private const string ResponseObj = "webAuthnLoginAssertionOptions";

public WebAuthnLoginAssertionOptionsResponseModel() : base(ResponseObj)
{
}

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

20 changes: 20 additions & 0 deletions src/Core/Auth/Models/Api/Response/UserDecryptionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ public UserDecryptionOptions() : base("userDecryptionOptions")
/// </summary>
public bool HasMasterPassword { get; set; }

/// <summary>
/// Gets or sets the WebAuthn PRF decryption keys.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public WebAuthnPrfDecryptionOption? WebAuthnPrfOptions { get; set; }
Comment on lines +22 to +23
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 init-only property for WebAuthnPrfOptions to ensure immutability after initialization


/// <summary>
/// Gets or sets information regarding this users trusted device decryption setup.
/// </summary>
Expand All @@ -29,6 +35,20 @@ public UserDecryptionOptions() : base("userDecryptionOptions")
public KeyConnectorUserDecryptionOption? KeyConnectorOption { get; set; }
}

public class WebAuthnPrfDecryptionOption
{
public string EncryptedPrivateKey { get; }
public string EncryptedUserKey { get; }

public WebAuthnPrfDecryptionOption(
string encryptedPrivateKey,
string encryptedUserKey)
{
EncryptedPrivateKey = encryptedPrivateKey;
EncryptedUserKey = encryptedUserKey;
}
}
Comment on lines +38 to +50
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 comments for WebAuthnPrfDecryptionOption class and its properties


public class TrustedDeviceUserDecryptionOption
{
public bool HasAdminApproval { get; }
Expand Down
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;
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 clarity

public const string ClearTextPrefix = "BWWebAuthnCredentialCreateOptions_";
public const string DataProtectorPurpose = "WebAuthnCredentialCreateDataProtector";
public const string TokenIdentifier = "WebAuthnCredentialCreateOptionsToken";

public string Identifier { get; set; } = TokenIdentifier;
Copy link

Choose a reason for hiding this comment

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

style: Identifier property can be made read-only

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;
}
Comment on lines +32 to +40
Copy link

Choose a reason for hiding this comment

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

style: TokenIsValid(User) method may be redundant with base class Valid property


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

Loading
Loading