Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Add passkey support to Identity module.#24278

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
maliming wants to merge3 commits intodev
base:dev
Choose a base branch
Loading
fromPasskey
Open
Show file tree
Hide file tree
Changes from1 commit
Commits
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
NextNext commit
Add passkey support to Identity module.
  • Loading branch information
@maliming
maliming committedNov 26, 2025
commitf372e5dcebd18ba713e5909e24e32ef00d009ebc
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
namespaceVolo.Abp.Identity;

publicstaticclassIdentityUserPasskeyConsts
{
/// <summary>
/// Default value: 1024
/// </summary>
publicstaticintMaxCredentialIdLength{get;set;}=1024;
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -160,4 +160,9 @@ Task UpdateOrganizationAsync(
Task<List<IdentityUserIdWithRoleNames>>GetRoleNamesAsync(
IEnumerable<Guid>userIds,
CancellationTokencancellationToken=default);

Task<IdentityUser>FindByPasskeyIdAsync(
byte[]credentialId,
boolincludeDetails=true,
CancellationTokencancellationToken=default);
}
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -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; }
}
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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()
{
}
Expand DownExpand Up@@ -188,6 +193,7 @@ public IdentityUser(
Tokens = new Collection<IdentityUserToken>();
OrganizationUnits = new Collection<IdentityUserOrganizationUnit>();
PasswordHistories = new Collection<IdentityUserPasswordHistory>();
Passkeys = new Collection<IdentityUserPasskey>();
}

public virtual void AddRole(Guid roleId)
Expand DownExpand Up@@ -403,6 +409,22 @@ public virtual void SetLastPasswordChangeTime(DateTimeOffset? lastPasswordChange
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}";
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -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 };
}
}
Original file line numberDiff line numberDiff line change
@@ -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
};
}
}
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@ public class IdentityUserStore :
IUserAuthenticationTokenStore<IdentityUser>,
IUserAuthenticatorKeyStore<IdentityUser>,
IUserTwoFactorRecoveryCodeStore<IdentityUser>,
IUserPasskeyStore<IdentityUser>,
ITransientDependency
{
private const string InternalLoginProvider = "[AspNetUserStore]";
Expand DownExpand Up@@ -1123,6 +1124,110 @@ public virtual Task<string> GetRecoveryCodeTokenNameAsync()
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()
{

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -94,6 +94,15 @@ into gp
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)
Expand DownExpand Up@@ -468,11 +477,11 @@ protected virtual async Task<IQueryable<IdentityUser>> GetFilteredQueryableAsync
{
var upperFilter = filter?.ToUpperInvariant();
var query = await GetQueryableAsync();

if (id.HasValue)
{
return query.Where(x => x.Id == id);
}
}

if (roleId.HasValue)
{
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,6 +47,7 @@ public static void ConfigureIdentity([NotNull] this ModelBuilder builder)
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);
Expand DownExpand Up@@ -176,6 +177,20 @@ public static void ConfigureIdentity([NotNull] this ModelBuilder builder)
});
}

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);
Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp