1
0
mirror of https://github.com/bitwarden/server synced 2026-02-26 09:23:28 +00:00

[PM-29890] Refactor Two-factor WebAuthn Methods Out of UserService (#6920)

* refactor(2fa-webauthn) [PM-29890]: Add command for start 2fa WebAuthn.

* refactor(2fa-webauthn) [PM-29890]: Put files into iface-root /implementations structure to align with other feature areas.

* refactor(2fa-webauthn) [PM-29890]: Add complete WebAuthn registration command.

* test(2fa-webauthn) [PM-29890]: Refactor and imrove 2fa WebAuthn testing from UserService.

* refactor(2fa-webauthn) [PM-29890]: Add delete WebAuthn credential command.

* test(2fa-webauthn) [PM-29890]: Add tests for delete WebAuthn credential command.

* refactor(2fa-webauthn) [PM-29890]: Update docs.

* refctor(2fa-webauthn) [PM-29890]: Re-spell docs.

* refactor(2fa-webauthn) [PM-29890]: Add comment around last-credential deletion.
This commit is contained in:
Dave
2026-02-13 13:35:42 -05:00
committed by GitHub
parent 84521a67c8
commit ca35b9e26f
17 changed files with 740 additions and 363 deletions

View File

@@ -0,0 +1,157 @@
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations;
using Bit.Core.Billing.Premium.Queries;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Fido2NetLib;
using Fido2NetLib.Objects;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Auth.UserFeatures.TwoFactorAuth;
[SutProviderCustomize]
public class CompleteTwoFactorWebAuthnRegistrationCommandTests
{
/// <summary>
/// The "Start" command will have set the in-process credential registration request to "pending" status.
/// The purpose of Complete is to consume and enshrine this pending credential.
/// </summary>
private static void SetupWebAuthnProviderWithPending(User user, int credentialCount)
{
var providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
var metadata = new Dictionary<string, object>();
// Add existing credentials
for (var i = 1; i <= credentialCount; i++)
{
metadata[$"Key{i}"] = new TwoFactorProvider.WebAuthnData
{
Name = $"Key {i}",
Descriptor = new PublicKeyCredentialDescriptor([(byte)i]),
PublicKey = [(byte)i],
UserHandle = [(byte)i],
SignatureCounter = 0,
CredType = "public-key",
RegDate = DateTime.UtcNow,
AaGuid = Guid.NewGuid()
};
}
// Add pending registration
var pendingOptions = new CredentialCreateOptions
{
Challenge = [1, 2, 3],
Rp = new PublicKeyCredentialRpEntity("example.com", "example.com", ""),
User = new Fido2User
{
Id = user.Id.ToByteArray(),
Name = user.Email ?? "test@example.com",
DisplayName = user.Name ?? "Test User"
},
PubKeyCredParams = []
};
metadata["pending"] = pendingOptions.ToJson();
providers[TwoFactorProviderType.WebAuthn] = new TwoFactorProvider { Enabled = true, MetaData = metadata };
user.SetTwoFactorProviders(providers);
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task CompleteWebAuthRegistrationAsync_BelowLimit_Succeeds(bool hasPremium,
SutProvider<CompleteTwoFactorWebAuthnRegistrationCommand> sutProvider, User user,
AuthenticatorAttestationRawResponse deviceResponse)
{
// Arrange - User should have 1 available credential, determined by maximum allowed for tested Premium status.
var maximumAllowedCredentialsGlobalSetting = new Core.Settings.GlobalSettings.WebAuthnSettings
{
PremiumMaximumAllowedCredentials = 10,
NonPremiumMaximumAllowedCredentials = 5
};
sutProvider.GetDependency<IGlobalSettings>().WebAuthn = maximumAllowedCredentialsGlobalSetting;
user.Premium = hasPremium;
user.Id = Guid.NewGuid();
user.Email = "test@example.com";
sutProvider.GetDependency<IHasPremiumAccessQuery>().HasPremiumAccessAsync(user.Id).Returns(hasPremium);
SetupWebAuthnProviderWithPending(user,
credentialCount: hasPremium
? maximumAllowedCredentialsGlobalSetting.PremiumMaximumAllowedCredentials - 1
: maximumAllowedCredentialsGlobalSetting.NonPremiumMaximumAllowedCredentials - 1);
var mockFido2 = sutProvider.GetDependency<IFido2>();
mockFido2.MakeNewCredentialAsync(
Arg.Any<AuthenticatorAttestationRawResponse>(),
Arg.Any<CredentialCreateOptions>(),
Arg.Any<IsCredentialIdUniqueToUserAsyncDelegate>())
.Returns(new Fido2.CredentialMakeResult("ok", "",
new AttestationVerificationSuccess
{
Aaguid = Guid.NewGuid(),
Counter = 0,
CredentialId = [1, 2, 3],
CredType = "public-key",
PublicKey = [4, 5, 6],
Status = "ok",
User = new Fido2User
{
Id = user.Id.ToByteArray(),
Name = user.Email ?? "test@example.com",
DisplayName = user.Name ?? "Test User"
}
}));
// Act
var result =
await sutProvider.Sut.CompleteTwoFactorWebAuthnRegistrationAsync(user, 5, "NewKey", deviceResponse);
// Assert
// Note that, contrary to the "Start" command, "Complete" does not suppress logging for the update providers invocation.
Assert.True(result);
await sutProvider.GetDependency<IUserService>().Received(1)
.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn);
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task CompleteWebAuthRegistrationAsync_ExceedsLimit_ThrowsBadRequestException(bool hasPremium,
SutProvider<CompleteTwoFactorWebAuthnRegistrationCommand> sutProvider, User user,
AuthenticatorAttestationRawResponse deviceResponse)
{
// Arrange - time-of-check/time-of-use scenario: user now has 10 credentials (at limit)
var maximumAllowedCredentialsGlobalSetting = new Core.Settings.GlobalSettings.WebAuthnSettings
{
PremiumMaximumAllowedCredentials = 10,
NonPremiumMaximumAllowedCredentials = 5
};
sutProvider.GetDependency<IGlobalSettings>().WebAuthn = maximumAllowedCredentialsGlobalSetting;
user.Premium = hasPremium;
sutProvider.GetDependency<IHasPremiumAccessQuery>().HasPremiumAccessAsync(user.Id).Returns(hasPremium);
SetupWebAuthnProviderWithPending(user,
credentialCount: hasPremium
? maximumAllowedCredentialsGlobalSetting.PremiumMaximumAllowedCredentials
: maximumAllowedCredentialsGlobalSetting.NonPremiumMaximumAllowedCredentials);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CompleteTwoFactorWebAuthnRegistrationAsync(user, 11, "NewKey", deviceResponse));
Assert.Equal("Maximum allowed WebAuthn credential count exceeded.", exception.Message);
}
}

View File

@@ -0,0 +1,138 @@
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations;
using Bit.Core.Entities;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Fido2NetLib.Objects;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Auth.UserFeatures.TwoFactorAuth;
[SutProviderCustomize]
public class DeleteTwoFactorWebAuthnCredentialCommandTests
{
private static void SetupWebAuthnProvider(User user, int credentialCount)
{
var providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
var metadata = new Dictionary<string, object>();
// Add credentials as Key1, Key2, Key3, etc.
for (var i = 1; i <= credentialCount; i++)
{
metadata[$"Key{i}"] = new TwoFactorProvider.WebAuthnData
{
Name = $"Key {i}",
Descriptor = new PublicKeyCredentialDescriptor([(byte)i]),
PublicKey = [(byte)i],
UserHandle = [(byte)i],
SignatureCounter = 0,
CredType = "public-key",
RegDate = DateTime.UtcNow,
AaGuid = Guid.NewGuid()
};
}
providers[TwoFactorProviderType.WebAuthn] = new TwoFactorProvider { Enabled = true, MetaData = metadata };
user.SetTwoFactorProviders(providers);
}
/// <summary>
/// When the user has multiple WebAuthn credentials and requests deletion of an existing key,
/// the command should remove it, persist via UserService, and return true.
/// </summary>
[Theory, BitAutoData]
public async Task DeleteAsync_KeyExistsWithMultipleKeys_RemovesKeyAndReturnsTrue(
SutProvider<DeleteTwoFactorWebAuthnCredentialCommand> sutProvider, User user)
{
// Arrange
SetupWebAuthnProvider(user, 3);
var keyIdToDelete = 2;
// Act
var result = await sutProvider.Sut.DeleteTwoFactorWebAuthnCredentialAsync(user, keyIdToDelete);
// Assert
Assert.True(result);
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
Assert.NotNull(provider?.MetaData);
Assert.False(provider.MetaData.ContainsKey($"Key{keyIdToDelete}"));
Assert.Equal(2, provider.MetaData.Count);
await sutProvider.GetDependency<IUserService>().Received(1)
.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn);
}
/// <summary>
/// When the requested key does not exist, the command should return false
/// and not call UserService.
/// </summary>
[Theory, BitAutoData]
public async Task DeleteAsync_KeyDoesNotExist_ReturnsFalse(
SutProvider<DeleteTwoFactorWebAuthnCredentialCommand> sutProvider, User user)
{
// Arrange
SetupWebAuthnProvider(user, 2);
var nonExistentKeyId = 99;
// Act
var result = await sutProvider.Sut.DeleteTwoFactorWebAuthnCredentialAsync(user, nonExistentKeyId);
// Assert
Assert.False(result);
await sutProvider.GetDependency<IUserService>().DidNotReceive()
.UpdateTwoFactorProviderAsync(Arg.Any<User>(), Arg.Any<TwoFactorProviderType>());
}
/// <summary>
/// Users must retain at least one WebAuthn credential. When only one key remains,
/// deletion should be rejected to prevent lockout.
/// </summary>
[Theory, BitAutoData]
public async Task DeleteAsync_OnlyOneKeyRemaining_ReturnsFalse(
SutProvider<DeleteTwoFactorWebAuthnCredentialCommand> sutProvider, User user)
{
// Arrange
SetupWebAuthnProvider(user, 1);
var keyIdToDelete = 1;
// Act
var result = await sutProvider.Sut.DeleteTwoFactorWebAuthnCredentialAsync(user, keyIdToDelete);
// Assert
Assert.False(result);
// Key should still exist
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
Assert.NotNull(provider?.MetaData);
Assert.True(provider.MetaData.ContainsKey($"Key{keyIdToDelete}"));
await sutProvider.GetDependency<IUserService>().DidNotReceive()
.UpdateTwoFactorProviderAsync(Arg.Any<User>(), Arg.Any<TwoFactorProviderType>());
}
/// <summary>
/// When the user has no two-factor providers configured, deletion should return false.
/// </summary>
[Theory, BitAutoData]
public async Task DeleteAsync_NoProviders_ReturnsFalse(
SutProvider<DeleteTwoFactorWebAuthnCredentialCommand> sutProvider, User user)
{
// Arrange - user with no providers (clear any AutoFixture-generated ones)
user.SetTwoFactorProviders(null);
// Act
var result = await sutProvider.Sut.DeleteTwoFactorWebAuthnCredentialAsync(user, 1);
// Assert
Assert.False(result);
await sutProvider.GetDependency<IUserService>().DidNotReceive()
.UpdateTwoFactorProviderAsync(Arg.Any<User>(), Arg.Any<TwoFactorProviderType>());
}
}

View File

@@ -0,0 +1,147 @@
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations;
using Bit.Core.Billing.Premium.Queries;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Fido2NetLib;
using Fido2NetLib.Objects;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Auth.UserFeatures.TwoFactorAuth;
[SutProviderCustomize]
public class StartTwoFactorWebAuthnRegistrationCommandTests
{
private static void SetupWebAuthnProvider(User user, int credentialCount)
{
var providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
var metadata = new Dictionary<string, object>();
// Add credentials as Key1, Key2, Key3, etc.
for (var i = 1; i <= credentialCount; i++)
{
metadata[$"Key{i}"] = new TwoFactorProvider.WebAuthnData
{
Name = $"Key {i}",
Descriptor = new PublicKeyCredentialDescriptor([(byte)i]),
PublicKey = [(byte)i],
UserHandle = [(byte)i],
SignatureCounter = 0,
CredType = "public-key",
RegDate = DateTime.UtcNow,
AaGuid = Guid.NewGuid()
};
}
providers[TwoFactorProviderType.WebAuthn] = new TwoFactorProvider { Enabled = true, MetaData = metadata };
user.SetTwoFactorProviders(providers);
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task StartWebAuthnRegistrationAsync_BelowLimit_Succeeds(
bool hasPremium, SutProvider<StartTwoFactorWebAuthnRegistrationCommand> sutProvider, User user)
{
// Arrange - User should have 1 available credential, determined by maximum allowed for tested Premium status.
var maximumAllowedCredentialsGlobalSetting = new Core.Settings.GlobalSettings.WebAuthnSettings
{
PremiumMaximumAllowedCredentials = 10,
NonPremiumMaximumAllowedCredentials = 5
};
sutProvider.GetDependency<IGlobalSettings>().WebAuthn = maximumAllowedCredentialsGlobalSetting;
user.Premium = hasPremium;
user.Id = Guid.NewGuid();
user.Email = "test@example.com";
sutProvider.GetDependency<IHasPremiumAccessQuery>().HasPremiumAccessAsync(user.Id).Returns(hasPremium);
SetupWebAuthnProvider(user,
credentialCount: hasPremium
? maximumAllowedCredentialsGlobalSetting.PremiumMaximumAllowedCredentials - 1
: maximumAllowedCredentialsGlobalSetting.NonPremiumMaximumAllowedCredentials - 1);
var mockFido2 = sutProvider.GetDependency<IFido2>();
mockFido2.RequestNewCredential(
Arg.Any<Fido2User>(),
Arg.Any<List<PublicKeyCredentialDescriptor>>(),
Arg.Any<AuthenticatorSelection>(),
Arg.Any<AttestationConveyancePreference>())
.Returns(new CredentialCreateOptions
{
Challenge = [1, 2, 3],
Rp = new PublicKeyCredentialRpEntity("example.com", "example.com", ""),
User = new Fido2User { Id = user.Id.ToByteArray(), Name = user.Email, DisplayName = user.Name },
PubKeyCredParams = []
});
// Act
var result = await sutProvider.Sut.StartTwoFactorWebAuthnRegistrationAsync(user);
// Assert
Assert.NotNull(result);
await sutProvider.GetDependency<IUserService>().Received(1)
.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn, false);
}
/// <summary>
/// "Start" provides the first half of a two-part process for registering a new WebAuthn 2FA credential.
/// To provide the best (most aggressive) UX possible, "Start" performs boundary validation of the ability to engage
/// in this flow based on current number of configured credentials. If the user is out of available credential slots,
/// Start should throw a BadRequestException for the client to handle.
/// </summary>
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task StartWebAuthnRegistrationAsync_ExceedsLimit_ThrowsBadRequestException(
bool hasPremium, SutProvider<StartTwoFactorWebAuthnRegistrationCommand> sutProvider, User user)
{
// Arrange - User should have 1 available credential, determined by maximum allowed for tested Premium status.
var maximumAllowedCredentialsGlobalSetting = new Core.Settings.GlobalSettings.WebAuthnSettings
{
PremiumMaximumAllowedCredentials = 10,
NonPremiumMaximumAllowedCredentials = 5
};
sutProvider.GetDependency<IGlobalSettings>().WebAuthn = maximumAllowedCredentialsGlobalSetting;
user.Premium = hasPremium;
user.Id = Guid.NewGuid();
user.Email = "test@example.com";
sutProvider.GetDependency<IHasPremiumAccessQuery>().HasPremiumAccessAsync(user.Id).Returns(hasPremium);
SetupWebAuthnProvider(user,
credentialCount: hasPremium
? maximumAllowedCredentialsGlobalSetting.PremiumMaximumAllowedCredentials
: maximumAllowedCredentialsGlobalSetting.NonPremiumMaximumAllowedCredentials);
var mockFido2 = sutProvider.GetDependency<IFido2>();
mockFido2.RequestNewCredential(
Arg.Any<Fido2User>(),
Arg.Any<List<PublicKeyCredentialDescriptor>>(),
Arg.Any<AuthenticatorSelection>(),
Arg.Any<AttestationConveyancePreference>())
.Returns(new CredentialCreateOptions
{
Challenge = [1, 2, 3],
Rp = new PublicKeyCredentialRpEntity("example.com", "example.com", ""),
User = new Fido2User { Id = user.Id.ToByteArray(), Name = user.Email, DisplayName = user.Name },
PubKeyCredParams = []
});
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.StartTwoFactorWebAuthnRegistrationAsync(user));
Assert.Equal("Maximum allowed WebAuthn credential count exceeded.", exception.Message);
}
}

View File

@@ -1,6 +1,6 @@
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations;
using Bit.Core.Billing.Premium.Queries;
using Bit.Core.Entities;
using Bit.Core.Exceptions;

View File

@@ -25,15 +25,11 @@ using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using Fido2NetLib;
using Fido2NetLib.Objects;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using NSubstitute;
using Xunit;
using static Fido2NetLib.Fido2;
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
namespace Bit.Core.Test.Services;
@@ -598,209 +594,6 @@ public class UserServiceTests
user.MasterPassword = null;
}
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task StartWebAuthnRegistrationAsync_BelowLimit_Succeeds(
bool hasPremium, SutProvider<UserService> sutProvider, User user)
{
// Arrange - Non-premium user with 4 credentials (below limit of 5)
SetupWebAuthnProvider(user, credentialCount: 4);
sutProvider.GetDependency<IGlobalSettings>().WebAuthn = new GlobalSettings.WebAuthnSettings
{
PremiumMaximumAllowedCredentials = 10,
NonPremiumMaximumAllowedCredentials = 5
};
user.Premium = hasPremium;
user.Id = Guid.NewGuid();
user.Email = "test@example.com";
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByUserAsync(user.Id)
.Returns(new List<OrganizationUser>());
var mockFido2 = sutProvider.GetDependency<IFido2>();
mockFido2.RequestNewCredential(
Arg.Any<Fido2User>(),
Arg.Any<List<PublicKeyCredentialDescriptor>>(),
Arg.Any<AuthenticatorSelection>(),
Arg.Any<AttestationConveyancePreference>())
.Returns(new CredentialCreateOptions
{
Challenge = new byte[] { 1, 2, 3 },
Rp = new PublicKeyCredentialRpEntity("example.com", "example.com", ""),
User = new Fido2User
{
Id = user.Id.ToByteArray(),
Name = user.Email,
DisplayName = user.Name
},
PubKeyCredParams = new List<PubKeyCredParam>()
});
// Act
var result = await sutProvider.Sut.StartWebAuthnRegistrationAsync(user);
// Assert
Assert.NotNull(result);
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(user);
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task CompleteWebAuthRegistrationAsync_ExceedsLimit_ThrowsBadRequestException(bool hasPremium,
SutProvider<UserService> sutProvider, User user, AuthenticatorAttestationRawResponse deviceResponse)
{
// Arrange - time-of-check/time-of-use scenario: user now has 10 credentials (at limit)
SetupWebAuthnProviderWithPending(user, credentialCount: 10);
sutProvider.GetDependency<IGlobalSettings>().WebAuthn = new GlobalSettings.WebAuthnSettings
{
PremiumMaximumAllowedCredentials = 10,
NonPremiumMaximumAllowedCredentials = 5
};
user.Premium = hasPremium;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByUserAsync(user.Id)
.Returns(new List<OrganizationUser>());
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.CompleteWebAuthRegistrationAsync(user, 11, "NewKey", deviceResponse));
Assert.Equal("Maximum allowed WebAuthn credential count exceeded.", exception.Message);
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task CompleteWebAuthRegistrationAsync_BelowLimit_Succeeds(bool hasPremium,
SutProvider<UserService> sutProvider, User user, AuthenticatorAttestationRawResponse deviceResponse)
{
// Arrange - User has 4 credentials (below limit of 5)
SetupWebAuthnProviderWithPending(user, credentialCount: 4);
sutProvider.GetDependency<IGlobalSettings>().WebAuthn = new GlobalSettings.WebAuthnSettings
{
PremiumMaximumAllowedCredentials = 10,
NonPremiumMaximumAllowedCredentials = 5
};
user.Premium = hasPremium;
user.Id = Guid.NewGuid();
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByUserAsync(user.Id)
.Returns(new List<OrganizationUser>());
var mockFido2 = sutProvider.GetDependency<IFido2>();
mockFido2.MakeNewCredentialAsync(
Arg.Any<AuthenticatorAttestationRawResponse>(),
Arg.Any<CredentialCreateOptions>(),
Arg.Any<IsCredentialIdUniqueToUserAsyncDelegate>())
.Returns(new CredentialMakeResult("ok", "", new AttestationVerificationSuccess
{
Aaguid = Guid.NewGuid(),
Counter = 0,
CredentialId = new byte[] { 1, 2, 3 },
CredType = "public-key",
PublicKey = new byte[] { 4, 5, 6 },
Status = "ok",
User = new Fido2User
{
Id = user.Id.ToByteArray(),
Name = user.Email ?? "test@example.com",
DisplayName = user.Name ?? "Test User"
}
}));
// Act
var result = await sutProvider.Sut.CompleteWebAuthRegistrationAsync(user, 5, "NewKey", deviceResponse);
// Assert
Assert.True(result);
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(user);
}
private static void SetupWebAuthnProvider(User user, int credentialCount)
{
var providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
var metadata = new Dictionary<string, object>();
// Add credentials as Key1, Key2, Key3, etc.
for (int i = 1; i <= credentialCount; i++)
{
metadata[$"Key{i}"] = new TwoFactorProvider.WebAuthnData
{
Name = $"Key {i}",
Descriptor = new PublicKeyCredentialDescriptor(new byte[] { (byte)i }),
PublicKey = new byte[] { (byte)i },
UserHandle = new byte[] { (byte)i },
SignatureCounter = 0,
CredType = "public-key",
RegDate = DateTime.UtcNow,
AaGuid = Guid.NewGuid()
};
}
providers[TwoFactorProviderType.WebAuthn] = new TwoFactorProvider
{
Enabled = true,
MetaData = metadata
};
user.SetTwoFactorProviders(providers);
}
private static void SetupWebAuthnProviderWithPending(User user, int credentialCount)
{
var providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
var metadata = new Dictionary<string, object>();
// Add existing credentials
for (int i = 1; i <= credentialCount; i++)
{
metadata[$"Key{i}"] = new TwoFactorProvider.WebAuthnData
{
Name = $"Key {i}",
Descriptor = new PublicKeyCredentialDescriptor(new byte[] { (byte)i }),
PublicKey = new byte[] { (byte)i },
UserHandle = new byte[] { (byte)i },
SignatureCounter = 0,
CredType = "public-key",
RegDate = DateTime.UtcNow,
AaGuid = Guid.NewGuid()
};
}
// Add pending registration
var pendingOptions = new CredentialCreateOptions
{
Challenge = new byte[] { 1, 2, 3 },
Rp = new PublicKeyCredentialRpEntity("example.com", "example.com", ""),
User = new Fido2User
{
Id = user.Id.ToByteArray(),
Name = user.Email ?? "test@example.com",
DisplayName = user.Name ?? "Test User"
},
PubKeyCredParams = new List<PubKeyCredParam>()
};
metadata["pending"] = pendingOptions.ToJson();
providers[TwoFactorProviderType.WebAuthn] = new TwoFactorProvider
{
Enabled = true,
MetaData = metadata
};
user.SetTwoFactorProviders(providers);
}
}
public static class UserServiceSutProviderExtensions