1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-05 18:12:48 -05:00

[PM-2032] Server endpoints to support authentication with a passkey (#3361)

* [PM-2032] feat: add assertion options tokenable

* [PM-2032] feat: add request and response models

* [PM-2032] feat: implement `assertion-options` identity endpoint

* [PM-2032] feat: implement authentication with passkey

* [PM-2032] chore: rename to `WebAuthnGrantValidator`

* [PM-2032] fix: add missing subsitute

* [PM-2032] feat: start adding builder

* [PM-2032] feat: add support for KeyConnector

* [PM-2032] feat: add first version of TDE

* [PM-2032] chore: refactor WithSso

* [PM-2023] feat: add support for TDE feature flag

* [PM-2023] feat: add support for approving devices

* [PM-2023] feat: add support for hasManageResetPasswordPermission

* [PM-2032] feat: add support for hasAdminApproval

* [PM-2032] chore: don't supply device if not necessary

* [PM-2032] chore: clean up imports

* [PM-2023] feat: extract interface

* [PM-2023] chore: add clarifying comment

* [PM-2023] feat: use new builder in production code

* [PM-2032] feat: add support for PRF

* [PM-2032] chore: clean-up todos

* [PM-2023] chore: remove token which is no longer used

* [PM-2032] chore: remove todo

* [PM-2032] feat: improve assertion error handling

* [PM-2032] fix: linting issues

* [PM-2032] fix: revert changes to `launchSettings.json`

* [PM-2023] chore: clean up assertion endpoint

* [PM-2032] feat: bypass 2FA

* [PM-2032] fix: rename prf option to singular

* [PM-2032] fix: lint

* [PM-2032] fix: typo

* [PM-2032] chore: improve builder tests

Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>

* [PM-2032] chore: clarify why we don't require 2FA

* [PM-2023] feat: move `identityProvider` constant to common class

* [PM-2032] fix: lint

* [PM-2023] fix: move `IdentityProvider` to core.Constants

* [PM-2032] fix: missing import

* [PM-2032] chore: refactor token timespan to use `TimeSpan`

* [PM-2032] chore: make `StartWebAuthnLoginAssertion` sync

* [PM-2032] chore: use `FromMinutes`

* [PM-2032] fix: change to 17 minutes to cover webauthn assertion

* [PM-2032] chore: do not use `async void`

* [PM-2032] fix: comment saying wrong amount of minutes

* [PM-2032] feat: put validator behind feature flag

* [PM-2032] fix: lint

---------

Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
This commit is contained in:
Andreas Coroiu
2023-11-20 15:55:31 +01:00
committed by GitHub
parent 07c202ecaf
commit 80740aa4ba
24 changed files with 855 additions and 223 deletions

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Auth.Enums;
public enum WebAuthnLoginAssertionOptionsScope
{
Authentication = 0,
PrfRegistration = 1
}

View File

@ -0,0 +1,18 @@

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

View File

@ -16,6 +16,12 @@ public class UserDecryptionOptions : ResponseModel
/// </summary>
public bool HasMasterPassword { get; set; }
/// <summary>
/// Gets or sets the WebAuthn PRF decryption keys.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public WebAuthnPrfDecryptionOption? WebAuthnPrfOption { get; set; }
/// <summary>
/// Gets or sets information regarding this users trusted device decryption setup.
/// </summary>
@ -29,6 +35,20 @@ public class UserDecryptionOptions : ResponseModel
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;
}
}
public class TrustedDeviceUserDecryptionOption
{
public bool HasAdminApproval { get; }

View File

@ -0,0 +1,47 @@
using System.Text.Json.Serialization;
using Bit.Core.Auth.Enums;
using Bit.Core.Tokens;
using Fido2NetLib;
namespace Bit.Core.Auth.Models.Business.Tokenables;
public class WebAuthnLoginAssertionOptionsTokenable : ExpiringTokenable
{
// Lifetime 17 minutes =
// - 6 Minutes for Attestation (max webauthn timeout)
// - 6 Minutes for PRF Assertion (max webauthn timeout)
// - 5 minutes for user to complete the process (name their passkey, etc)
private static readonly TimeSpan _tokenLifetime = TimeSpan.FromMinutes(17);
public const string ClearTextPrefix = "BWWebAuthnLoginAssertionOptions_";
public const string DataProtectorPurpose = "WebAuthnLoginAssertionOptionsDataProtector";
public const string TokenIdentifier = "WebAuthnLoginAssertionOptionsToken";
public string Identifier { get; set; } = TokenIdentifier;
public AssertionOptions Options { get; set; }
public WebAuthnLoginAssertionOptionsScope Scope { get; set; }
[JsonConstructor]
public WebAuthnLoginAssertionOptionsTokenable()
{
ExpirationDate = DateTime.UtcNow.Add(_tokenLifetime);
}
public WebAuthnLoginAssertionOptionsTokenable(WebAuthnLoginAssertionOptionsScope scope, AssertionOptions options) : this()
{
Scope = scope;
Options = options;
}
public bool TokenIsValid(WebAuthnLoginAssertionOptionsScope scope)
{
if (!Valid)
{
return false;
}
return Scope == scope;
}
protected override bool TokenIsValid() => Identifier == TokenIdentifier && Options != null;
}

View File

@ -1,43 +0,0 @@
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);
}

View File

@ -0,0 +1,19 @@
namespace Bit.Core.Auth.Utilities;
public static class GuidUtilities
{
public static bool TryParseBytes(ReadOnlySpan<byte> bytes, out Guid guid)
{
try
{
guid = new Guid(bytes);
return true;
}
catch
{
guid = Guid.Empty;
return false;
}
}
}