Browse Source

Add passkey support to Identity module.

pull/24278/head
maliming 5 months ago
parent
commit
f372e5dceb
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 9
      modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentityUserPasskeyConsts.cs
  2. 5
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityUserRepository.cs
  3. 68
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityPasskeyData.cs
  4. 22
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUser.cs
  5. 53
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserPasskey.cs
  6. 32
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserPasskeyExtensions.cs
  7. 105
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserStore.cs
  8. 13
      modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs
  9. 15
      modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs
  10. 3
      modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityEfCoreQueryableExtensions.cs
  11. 8
      modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs
  12. 101
      modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserStore_Tests.cs
  13. 3
      modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/AbpIdentityTestDataBuilder.cs
  14. 3
      modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityTestData.cs
  15. 17
      modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityUserRepository_Tests.cs

9
modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentityUserPasskeyConsts.cs

@ -0,0 +1,9 @@
namespace Volo.Abp.Identity;
public static class IdentityUserPasskeyConsts
{
/// <summary>
/// Default value: 1024
/// </summary>
public static int MaxCredentialIdLength { get; set; } = 1024;
}

5
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityUserRepository.cs

@ -160,4 +160,9 @@ public interface IIdentityUserRepository : IBasicRepository<IdentityUser, Guid>
Task<List<IdentityUserIdWithRoleNames>> GetRoleNamesAsync(
IEnumerable<Guid> userIds,
CancellationToken cancellationToken = default);
Task<IdentityUser> FindByPasskeyIdAsync(
byte[] credentialId,
bool includeDetails = true,
CancellationToken cancellationToken = default);
}

68
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityPasskeyData.cs

@ -0,0 +1,68 @@
using System;
namespace Volo.Abp.Identity;
/// <summary>
/// Represents data associated with a passkey.
/// </summary>
public class IdentityPasskeyData
{
/// <summary>
/// Gets or sets the public key associated with this passkey.
/// </summary>
public virtual byte[] PublicKey { get; set; }
/// <summary>
/// Gets or sets the friendly name for this passkey.
/// </summary>
public virtual string? Name { get; set; }
/// <summary>
/// Gets or sets the time this passkey was created.
/// </summary>
public virtual DateTimeOffset CreatedAt { get; set; }
/// <summary>
/// Gets or sets the signature counter for this passkey.
/// </summary>
public virtual uint SignCount { get; set; }
/// <summary>
/// Gets or sets the transports supported by this passkey.
/// </summary>
/// <remarks>
/// See <see href="https://www.w3.org/TR/webauthn-3/#enumdef-authenticatortransport"/>.
/// </remarks>
public virtual string[] Transports { get; set; }
/// <summary>
/// Gets or sets whether the passkey has a verified user.
/// </summary>
public virtual bool IsUserVerified { get; set; }
/// <summary>
/// Gets or sets whether the passkey is eligible for backup.
/// </summary>
public virtual bool IsBackupEligible { get; set; }
/// <summary>
/// Gets or sets whether the passkey is currently backed up.
/// </summary>
public virtual bool IsBackedUp { get; set; }
/// <summary>
/// Gets or sets the attestation object associated with this passkey.
/// </summary>
/// <remarks>
/// See <see href="https://www.w3.org/TR/webauthn-3/#attestation-object"/>.
/// </remarks>
public virtual byte[] AttestationObject { get; set; }
/// <summary>
/// Gets or sets the collected client data JSON associated with this passkey.
/// </summary>
/// <remarks>
/// See <see href="https://www.w3.org/TR/webauthn-3/#dictdef-collectedclientdata"/>.
/// </remarks>
public virtual byte[] ClientDataJson { get; set; }
}

22
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUser.cs

@ -159,6 +159,11 @@ public class IdentityUser : FullAuditedAggregateRoot<Guid>, IUser, IHasEntityVer
/// </summary>
public virtual ICollection<IdentityUserPasswordHistory> PasswordHistories { get; protected set; }
/// <summary>
/// Navigation property for this users passkeys.
/// </summary>
public virtual ICollection<IdentityUserPasskey> Passkeys { get; protected set; }
protected IdentityUser()
{
}
@ -188,6 +193,7 @@ public class IdentityUser : FullAuditedAggregateRoot<Guid>, IUser, IHasEntityVer
Tokens = new Collection<IdentityUserToken>();
OrganizationUnits = new Collection<IdentityUserOrganizationUnit>();
PasswordHistories = new Collection<IdentityUserPasswordHistory>();
Passkeys = new Collection<IdentityUserPasskey>();
}
public virtual void AddRole(Guid roleId)
@ -403,6 +409,22 @@ public class IdentityUser : FullAuditedAggregateRoot<Guid>, IUser, IHasEntityVer
LastPasswordChangeTime = lastPasswordChangeTime;
}
[CanBeNull]
public virtual IdentityUserPasskey FindPasskey(byte[] credentialId)
{
return Passkeys.FirstOrDefault(x => x.UserId == Id && x.CredentialId.SequenceEqual(credentialId));
}
public virtual void AddPasskey(byte[] credentialId, IdentityPasskeyData passkeyData)
{
Passkeys.Add(new IdentityUserPasskey(Id, credentialId, passkeyData, TenantId));
}
public virtual void RemovePasskey(byte[] credentialId)
{
Passkeys.RemoveAll(x => x.CredentialId.SequenceEqual(credentialId));
}
public override string ToString()
{
return $"{base.ToString()}, UserName = {UserName}";

53
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserPasskey.cs

@ -0,0 +1,53 @@
using System;
using Volo.Abp.Domain.Entities;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Identity;
/// <summary>
/// Represents a passkey credential for a user in the identity system.
/// </summary>
/// <remarks>
/// See <see href="https://www.w3.org/TR/webauthn-3/#credential-record"/>.
/// </remarks>
public class IdentityUserPasskey : Entity, IMultiTenant
{
public virtual Guid? TenantId { get; protected set; }
/// <summary>
/// Gets or sets the primary key of the user that owns this passkey.
/// </summary>
public virtual Guid UserId { get; protected set; }
/// <summary>
/// Gets or sets the credential ID for this passkey.
/// </summary>
public virtual byte[] CredentialId { get; set; }
/// <summary>
/// Gets or sets additional data associated with this passkey.
/// </summary>
public virtual IdentityPasskeyData Data { get; set; }
protected IdentityUserPasskey()
{
}
public IdentityUserPasskey(
Guid userId,
byte[] credentialId,
IdentityPasskeyData data,
Guid? tenantId)
{
UserId = userId;
CredentialId = credentialId;
Data = data;
TenantId = tenantId;
}
public override object[] GetKeys()
{
return new object[] { UserId, CredentialId };
}
}

32
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserPasskeyExtensions.cs

@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Identity;
namespace Volo.Abp.Identity;
public static class IdentityUserPasskeyExtensions
{
public static void UpdateFromUserPasskeyInfo(this IdentityUserPasskey passkey, UserPasskeyInfo passkeyInfo)
{
passkey.Data.Name = passkeyInfo.Name;
passkey.Data.SignCount = passkeyInfo.SignCount;
passkey.Data.IsBackedUp = passkeyInfo.IsBackedUp;
passkey.Data.IsUserVerified = passkeyInfo.IsUserVerified;
}
public static UserPasskeyInfo ToUserPasskeyInfo(this IdentityUserPasskey passkey)
{
return new UserPasskeyInfo(
passkey.CredentialId,
passkey.Data.PublicKey,
passkey.Data.CreatedAt,
passkey.Data.SignCount,
passkey.Data.Transports,
passkey.Data.IsUserVerified,
passkey.Data.IsBackupEligible,
passkey.Data.IsBackedUp,
passkey.Data.AttestationObject,
passkey.Data.ClientDataJson)
{
Name = passkey.Data.Name
};
}
}

105
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserStore.cs

@ -32,6 +32,7 @@ public class IdentityUserStore :
IUserAuthenticationTokenStore<IdentityUser>,
IUserAuthenticatorKeyStore<IdentityUser>,
IUserTwoFactorRecoveryCodeStore<IdentityUser>,
IUserPasskeyStore<IdentityUser>,
ITransientDependency
{
private const string InternalLoginProvider = "[AspNetUserStore]";
@ -1123,6 +1124,110 @@ public class IdentityUserStore :
return Task.FromResult(RecoveryCodeTokenName);
}
/// <summary>
/// Creates a new passkey credential in the store for the specified <paramref name="user"/>,
/// or updates an existing passkey.
/// </summary>
/// <param name="user">The user to create the passkey credential for.</param>
/// <param name="passkey"></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public virtual async Task AddOrUpdatePasskeyAsync(IdentityUser user, UserPasskeyInfo passkey, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
await UserRepository.EnsureCollectionLoadedAsync(user, u => u.Passkeys, cancellationToken);
var userPasskey = user.FindPasskey(passkey.CredentialId);
if (userPasskey != null)
{
userPasskey.UpdateFromUserPasskeyInfo(passkey);
}
else
{
user.AddPasskey(passkey.CredentialId, new IdentityPasskeyData()
{
PublicKey = passkey.PublicKey,
Name = passkey.Name,
CreatedAt = passkey.CreatedAt,
Transports = passkey.Transports,
SignCount = passkey.SignCount,
IsUserVerified = passkey.IsUserVerified,
IsBackupEligible = passkey.IsBackupEligible,
IsBackedUp = passkey.IsBackedUp,
AttestationObject = passkey.AttestationObject,
ClientDataJson = passkey.ClientDataJson,
});
}
}
/// <summary>
/// Gets the passkey credentials for the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user whose passkeys should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing a list of the user's passkeys.</returns>
public virtual async Task<IList<UserPasskeyInfo>> GetPasskeysAsync(IdentityUser user, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Check.NotNull(user, nameof(user));
await UserRepository.EnsureCollectionLoadedAsync(user, u => u.Passkeys, cancellationToken);
return user.Passkeys.Select(p => p.ToUserPasskeyInfo()).ToList();
}
/// <summary>
/// Finds and returns a user, if any, associated with the specified passkey credential identifier.
/// </summary>
/// <param name="credentialId">The passkey credential id to search for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> that represents the asynchronous operation, containing the user, if any, associated with the specified passkey credential id.
/// </returns>
public virtual async Task<IdentityUser> FindByPasskeyIdAsync(byte[] credentialId, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return await UserRepository.FindByPasskeyIdAsync(credentialId, cancellationToken: cancellationToken);
}
/// <summary>
/// Finds a passkey for the specified user with the specified credential id.
/// </summary>
/// <param name="user">The user whose passkey should be retrieved.</param>
/// <param name="credentialId">The credential id to search for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the user's passkey information.</returns>
public virtual async Task<UserPasskeyInfo> FindPasskeyAsync(IdentityUser user, byte[] credentialId, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Check.NotNull(user, nameof(user));
Check.NotNull(credentialId, nameof(credentialId));
await UserRepository.EnsureCollectionLoadedAsync(user, u => u.Passkeys, cancellationToken);
return user.FindPasskey(credentialId)?.ToUserPasskeyInfo();
}
/// <summary>
/// Removes a passkey credential from the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to remove the passkey credential from.</param>
/// <param name="credentialId">The credential id of the passkey to remove.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public virtual async Task RemovePasskeyAsync(IdentityUser user, byte[] credentialId, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Check.NotNull(user, nameof(user));
Check.NotNull(credentialId, nameof(credentialId));
await UserRepository.EnsureCollectionLoadedAsync(user, u => u.Passkeys, cancellationToken);
user.RemovePasskey(credentialId);
}
public virtual void Dispose()
{

13
modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs

@ -94,6 +94,15 @@ public class EfCoreIdentityUserRepository : EfCoreRepository<IIdentityDbContext,
return userRoles.Concat(orgUnitRoles).GroupBy(x => x.Id).Select(x => new IdentityUserIdWithRoleNames { Id = x.Key, RoleNames = x.SelectMany(y => y.RoleNames).Distinct().ToArray() }).ToList();
}
public virtual async Task<IdentityUser> FindByPasskeyIdAsync(byte[] credentialId, bool includeDetails = true, CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync())
.IncludeDetails(includeDetails)
.Where(u => u.Passkeys.Any(x => x.CredentialId.SequenceEqual(credentialId)))
.OrderBy(x => x.Id)
.FirstOrDefaultAsync(GetCancellationToken(cancellationToken));
}
public virtual async Task<List<string>> GetRoleNamesInOrganizationUnitAsync(
Guid id,
CancellationToken cancellationToken = default)
@ -468,11 +477,11 @@ public class EfCoreIdentityUserRepository : EfCoreRepository<IIdentityDbContext,
{
var upperFilter = filter?.ToUpperInvariant();
var query = await GetQueryableAsync();
if (id.HasValue)
{
return query.Where(x => x.Id == id);
}
}
if (roleId.HasValue)
{

15
modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs

@ -47,6 +47,7 @@ public static class IdentityDbContextModelBuilderExtensions
b.HasMany(u => u.Tokens).WithOne().HasForeignKey(ur => ur.UserId).IsRequired();
b.HasMany(u => u.OrganizationUnits).WithOne().HasForeignKey(ur => ur.UserId).IsRequired();
b.HasMany(u => u.PasswordHistories).WithOne().HasForeignKey(ur => ur.UserId).IsRequired();
b.HasMany(u => u.Passkeys).WithOne().HasForeignKey(ur => ur.UserId).IsRequired();
b.HasIndex(u => u.NormalizedUserName);
b.HasIndex(u => u.NormalizedEmail);
@ -176,6 +177,20 @@ public static class IdentityDbContextModelBuilderExtensions
});
}
builder.Entity<IdentityUserPasskey>(b =>
{
b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "UserPasskeys", AbpIdentityDbProperties.DbSchema);
b.ConfigureByConvention();
b.HasKey(p => p.CredentialId);
b.Property(p => p.CredentialId).HasMaxLength(IdentityUserPasskeyConsts.MaxCredentialIdLength); // Defined in WebAuthn spec to be no longer than 1023 bytes
b.OwnsOne(p => p.Data).ToJson();
b.ApplyObjectExtensionMappings();
});
builder.Entity<OrganizationUnit>(b =>
{
b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "OrganizationUnits", AbpIdentityDbProperties.DbSchema);

3
modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityEfCoreQueryableExtensions.cs

@ -18,7 +18,8 @@ public static class IdentityEfCoreQueryableExtensions
.Include(x => x.Claims)
.Include(x => x.Tokens)
.Include(x => x.OrganizationUnits)
.Include(x => x.PasswordHistories);
.Include(x => x.PasswordHistories)
.Include(x => x.Passkeys);
}
public static IQueryable<IdentityRole> IncludeDetails(this IQueryable<IdentityRole> queryable, bool include = true)

8
modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs

@ -443,6 +443,14 @@ public class MongoIdentityUserRepository : MongoDbRepository<IAbpIdentityMongoDb
return result;
}
public virtual async Task<IdentityUser> FindByPasskeyIdAsync(byte[] credentialId, bool includeDetails = true, CancellationToken cancellationToken = default)
{
return await (await GetQueryableAsync(cancellationToken))
.Where(u => u.Passkeys.Any(x => x.CredentialId == credentialId))
.OrderBy(x => x.Id)
.FirstOrDefaultAsync(GetCancellationToken(cancellationToken));
}
protected virtual async Task<IQueryable<IdentityUser>> GetFilteredQueryableAsync(
string filter = null,
Guid? roleId = null,

101
modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserStore_Tests.cs

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Shouldly;
@ -766,4 +768,103 @@ public class IdentityUserStore_Tests : AbpIdentityDomainTestBase
await uow.CompleteAsync();
}
}
[Fact]
public async Task AddOrUpdatePasskeyAsync()
{
using (var uow = _unitOfWorkManager.Begin())
{
var credentialId = (byte[]) [1, 2];
var user = await _identityUserStore.FindByIdAsync(_testData.UserBobId.ToString());
user.Passkeys.ShouldBeEmpty();
var passkey = new UserPasskeyInfo(credentialId, null!, default, 0, null, false, false, false, null!, null!);
await _identityUserStore.AddOrUpdatePasskeyAsync(user, passkey, CancellationToken.None);
user = await _identityUserStore.FindByIdAsync(_testData.UserBobId.ToString());
user.Passkeys.ShouldNotBeEmpty();
user.FindPasskey(credentialId).ShouldNotBeNull();
await uow.CompleteAsync();
}
}
[Fact]
public async Task GetPasskeysAsync()
{
using (var uow = _unitOfWorkManager.Begin())
{
var user = await _identityUserStore.FindByIdAsync(_testData.UserJohnId.ToString());
var passkeys = await _identityUserStore.GetPasskeysAsync(user, CancellationToken.None);
passkeys.Count.ShouldBe(2);
user = await _identityUserStore.FindByIdAsync(_testData.UserBobId.ToString());
passkeys = await _identityUserStore.GetPasskeysAsync(user, CancellationToken.None);
passkeys.ShouldBeEmpty();
await uow.CompleteAsync();
}
}
[Fact]
public async Task FindByPasskeyIdAsync()
{
using (var uow = _unitOfWorkManager.Begin())
{
var user = await _identityUserStore.FindByPasskeyIdAsync(_testData.PasskeyCredentialId1, CancellationToken.None);
user.ShouldNotBeNull();
user.Id.ShouldBe(_testData.UserJohnId);
user = await _identityUserStore.FindByPasskeyIdAsync(_testData.PasskeyCredentialId2, CancellationToken.None);
user.ShouldNotBeNull();
user.Id.ShouldBe(_testData.UserJohnId);
user = await _identityUserStore.FindByPasskeyIdAsync(_testData.PasskeyCredentialId3, CancellationToken.None);
user.ShouldNotBeNull();
user.Id.ShouldBe(_testData.UserNeoId);
await uow.CompleteAsync();
}
}
[Fact]
public async Task FindPasskeyAsync()
{
using (var uow = _unitOfWorkManager.Begin())
{
var user = await _identityUserStore.FindByIdAsync(_testData.UserJohnId.ToString());
var passkey = await _identityUserStore.FindPasskeyAsync(user, _testData.PasskeyCredentialId1, CancellationToken.None);
passkey.ShouldNotBeNull();
passkey.CredentialId.ShouldBe(_testData.PasskeyCredentialId1);
passkey = await _identityUserStore.FindPasskeyAsync(user, _testData.PasskeyCredentialId2, CancellationToken.None);
passkey.ShouldNotBeNull();
passkey.CredentialId.ShouldBe(_testData.PasskeyCredentialId2);
passkey = await _identityUserStore.FindPasskeyAsync(user, _testData.PasskeyCredentialId3, CancellationToken.None);
passkey.ShouldBeNull();
await uow.CompleteAsync();
}
}
[Fact]
public async Task RemovePasskeyAsync()
{
using (var uow = _unitOfWorkManager.Begin())
{
var user = await _identityUserStore.FindByIdAsync(_testData.UserJohnId.ToString());
user.Passkeys.Count.ShouldBe(2);
var credentialId = user.Passkeys.First().CredentialId;
await _identityUserStore.RemovePasskeyAsync(user, credentialId, CancellationToken.None);
user = await _identityUserStore.FindByIdAsync(_testData.UserJohnId.ToString());
user.Passkeys.Count.ShouldBe(1);
user.FindPasskey(credentialId).ShouldBeNull();
await uow.CompleteAsync();
}
}
}

3
modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/AbpIdentityTestDataBuilder.cs

@ -140,6 +140,8 @@ public class AbpIdentityTestDataBuilder : ITransientDependency
john.AddLogin(new UserLoginInfo("twitter", "johnx", "John Nash"));
john.AddClaim(_guidGenerator, new Claim("TestClaimType", "42"));
john.SetToken("test-provider", "test-name", "test-value");
john.AddPasskey(_testData.PasskeyCredentialId1, new IdentityPasskeyData());
john.AddPasskey(_testData.PasskeyCredentialId2, new IdentityPasskeyData());
await _userRepository.InsertAsync(john);
var david = new IdentityUser(_testData.UserDavidId, "david", "david@abp.io");
@ -152,6 +154,7 @@ public class AbpIdentityTestDataBuilder : ITransientDependency
neo.AddRole(_supporterRole.Id);
neo.AddClaim(_guidGenerator, new Claim("TestClaimType", "43"));
neo.AddOrganizationUnit(_ou111.Id);
neo.AddPasskey(_testData.PasskeyCredentialId3, new IdentityPasskeyData());
await _userRepository.InsertAsync(neo);
var bob = new IdentityUser(_testData.UserBobId, "bob", "bob@abp.io");

3
modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityTestData.cs

@ -15,4 +15,7 @@ public class IdentityTestData : ISingletonDependency
public Guid UserBobId { get; } = Guid.NewGuid();
public Guid AgeClaimId { get; } = Guid.NewGuid();
public Guid EducationClaimId { get; } = Guid.NewGuid();
public byte[] PasskeyCredentialId1 { get; } = (byte[])[1, 2, 3, 4, 5, 6, 7, 8];
public byte[] PasskeyCredentialId2 { get; } = (byte[])[8, 7, 6, 5, 4, 3, 2, 1];
public byte[] PasskeyCredentialId3 { get; } = (byte[])[1, 2, 3, 4, 8, 7, 6, 5,];
}

17
modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityUserRepository_Tests.cs

@ -159,7 +159,7 @@ public abstract class IdentityUserRepository_Tests<TStartupModule> : AbpIdentity
StringComparison.OrdinalIgnoreCase
).ShouldBeGreaterThan(0);
}
users = await UserRepository.GetListAsync(null, 5, 0, null, roleId: TestData.RoleManagerId);
users.ShouldContain(x => x.UserName == "john.nash");
users.ShouldContain(x => x.UserName == "neo");
@ -294,4 +294,19 @@ public abstract class IdentityUserRepository_Tests<TStartupModule> : AbpIdentity
ou112Users.ShouldContain(x => x.UserName == "john.nash");
ou112Users.ShouldContain(x => x.UserName == "neo");
}
[Fact]
public async Task FindByPasskeyIdAsync()
{
var user = await UserRepository.FindByPasskeyIdAsync(TestData.PasskeyCredentialId1);
user.ShouldNotBeNull();
user.Id.ShouldBe(TestData.UserJohnId);
user = await UserRepository.FindByPasskeyIdAsync(TestData.PasskeyCredentialId2);
user.ShouldNotBeNull();
user.Id.ShouldBe(TestData.UserNeoId);
(await UserRepository.FindByPasskeyIdAsync((byte[])[1, 2, 3])).ShouldBeNull();
}
}

Loading…
Cancel
Save