Skip to content

Commit 9b3858c

Browse files
Allow UserStore to update passkey name (#63981)
1 parent 28e5d34 commit 9b3858c

File tree

13 files changed

+401
-87
lines changed

13 files changed

+401
-87
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore;
5+
6+
internal static class IdentityUserPasskeyExtensions
7+
{
8+
extension<TKey>(IdentityUserPasskey<TKey> passkey)
9+
where TKey : IEquatable<TKey>
10+
{
11+
public void UpdateFromUserPasskeyInfo(UserPasskeyInfo passkeyInfo)
12+
{
13+
passkey.Data.Name = passkeyInfo.Name;
14+
passkey.Data.SignCount = passkeyInfo.SignCount;
15+
passkey.Data.IsBackedUp = passkeyInfo.IsBackedUp;
16+
passkey.Data.IsUserVerified = passkeyInfo.IsUserVerified;
17+
}
18+
19+
public UserPasskeyInfo ToUserPasskeyInfo()
20+
=> new(
21+
passkey.CredentialId,
22+
passkey.Data.PublicKey,
23+
passkey.Data.CreatedAt,
24+
passkey.Data.SignCount,
25+
passkey.Data.Transports,
26+
passkey.Data.IsUserVerified,
27+
passkey.Data.IsBackupEligible,
28+
passkey.Data.IsBackedUp,
29+
passkey.Data.AttestationObject,
30+
passkey.Data.ClientDataJson)
31+
{
32+
Name = passkey.Data.Name
33+
};
34+
}
35+
}

src/Identity/EntityFrameworkCore/src/UserOnlyStore.cs

Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -625,10 +625,7 @@ public virtual async Task AddOrUpdatePasskeyAsync(TUser user, UserPasskeyInfo pa
625625
var userPasskey = await FindUserPasskeyByIdAsync(passkey.CredentialId, cancellationToken).ConfigureAwait(false);
626626
if (userPasskey != null)
627627
{
628-
userPasskey.Data.Name = passkey.Name;
629-
userPasskey.Data.SignCount = passkey.SignCount;
630-
userPasskey.Data.IsBackedUp = passkey.IsBackedUp;
631-
userPasskey.Data.IsUserVerified = passkey.IsUserVerified;
628+
userPasskey.UpdateFromUserPasskeyInfo(passkey);
632629
UserPasskeys.Update(userPasskey);
633630
}
634631
else
@@ -655,20 +652,7 @@ public virtual async Task<IList<UserPasskeyInfo>> GetPasskeysAsync(TUser user, C
655652
var userId = user.Id;
656653
var passkeys = await UserPasskeys
657654
.Where(p => p.UserId.Equals(userId))
658-
.Select(p => new UserPasskeyInfo(
659-
p.CredentialId,
660-
p.Data.PublicKey,
661-
p.Data.CreatedAt,
662-
p.Data.SignCount,
663-
p.Data.Transports,
664-
p.Data.IsUserVerified,
665-
p.Data.IsBackupEligible,
666-
p.Data.IsBackedUp,
667-
p.Data.AttestationObject,
668-
p.Data.ClientDataJson)
669-
{
670-
Name = p.Data.Name,
671-
})
655+
.Select(p => p.ToUserPasskeyInfo())
672656
.ToListAsync(cancellationToken)
673657
.ConfigureAwait(false);
674658

@@ -708,26 +692,10 @@ public virtual async Task<IList<UserPasskeyInfo>> GetPasskeysAsync(TUser user, C
708692
cancellationToken.ThrowIfCancellationRequested();
709693
ThrowIfDisposed();
710694
ArgumentNullException.ThrowIfNull(user);
695+
ArgumentNullException.ThrowIfNull(credentialId);
711696

712697
var passkey = await FindUserPasskeyAsync(user.Id, credentialId, cancellationToken).ConfigureAwait(false);
713-
if (passkey != null)
714-
{
715-
return new UserPasskeyInfo(
716-
passkey.CredentialId,
717-
passkey.Data.PublicKey,
718-
passkey.Data.CreatedAt,
719-
passkey.Data.SignCount,
720-
passkey.Data.Transports,
721-
passkey.Data.IsUserVerified,
722-
passkey.Data.IsBackupEligible,
723-
passkey.Data.IsBackedUp,
724-
passkey.Data.AttestationObject,
725-
passkey.Data.ClientDataJson)
726-
{
727-
Name = passkey.Data.Name,
728-
};
729-
}
730-
return null;
698+
return passkey?.ToUserPasskeyInfo();
731699
}
732700

733701
/// <summary>

src/Identity/EntityFrameworkCore/src/UserStore.cs

Lines changed: 4 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -770,9 +770,7 @@ public virtual async Task AddOrUpdatePasskeyAsync(TUser user, UserPasskeyInfo pa
770770
var userPasskey = await FindUserPasskeyByIdAsync(passkey.CredentialId, cancellationToken).ConfigureAwait(false);
771771
if (userPasskey != null)
772772
{
773-
userPasskey.Data.SignCount = passkey.SignCount;
774-
userPasskey.Data.IsBackedUp = passkey.IsBackedUp;
775-
userPasskey.Data.IsUserVerified = passkey.IsUserVerified;
773+
userPasskey.UpdateFromUserPasskeyInfo(passkey);
776774
UserPasskeys.Update(userPasskey);
777775
}
778776
else
@@ -799,20 +797,7 @@ public virtual async Task<IList<UserPasskeyInfo>> GetPasskeysAsync(TUser user, C
799797
var userId = user.Id;
800798
var passkeys = await UserPasskeys
801799
.Where(p => p.UserId.Equals(userId))
802-
.Select(p => new UserPasskeyInfo(
803-
p.CredentialId,
804-
p.Data.PublicKey,
805-
p.Data.CreatedAt,
806-
p.Data.SignCount,
807-
p.Data.Transports,
808-
p.Data.IsUserVerified,
809-
p.Data.IsBackupEligible,
810-
p.Data.IsBackedUp,
811-
p.Data.AttestationObject,
812-
p.Data.ClientDataJson)
813-
{
814-
Name = p.Data.Name
815-
})
800+
.Select(p => p.ToUserPasskeyInfo())
816801
.ToListAsync(cancellationToken)
817802
.ConfigureAwait(false);
818803

@@ -851,27 +836,11 @@ public virtual async Task<IList<UserPasskeyInfo>> GetPasskeysAsync(TUser user, C
851836
{
852837
cancellationToken.ThrowIfCancellationRequested();
853838
ThrowIfDisposed();
839+
ArgumentNullException.ThrowIfNull(user);
854840
ArgumentNullException.ThrowIfNull(credentialId);
855841

856842
var passkey = await FindUserPasskeyAsync(user.Id, credentialId, cancellationToken).ConfigureAwait(false);
857-
if (passkey != null)
858-
{
859-
return new UserPasskeyInfo(
860-
passkey.CredentialId,
861-
passkey.Data.PublicKey,
862-
passkey.Data.CreatedAt,
863-
passkey.Data.SignCount,
864-
passkey.Data.Transports,
865-
passkey.Data.IsUserVerified,
866-
passkey.Data.IsBackupEligible,
867-
passkey.Data.IsBackedUp,
868-
passkey.Data.AttestationObject,
869-
passkey.Data.ClientDataJson)
870-
{
871-
Name = passkey.Data.Name
872-
};
873-
}
874-
return null;
843+
return passkey?.ToUserPasskeyInfo();
875844
}
876845

877846
/// <summary>

src/Identity/EntityFrameworkCore/test/EF.InMemory.Test/InMemoryContext.cs

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,31 @@
33

44
using System.Data.Common;
55
using Microsoft.EntityFrameworkCore;
6+
using Microsoft.Extensions.DependencyInjection;
67

78
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test;
89

910
public class InMemoryContext :
1011
InMemoryContext<IdentityUser, IdentityRole, string>
1112
{
12-
private InMemoryContext(DbConnection connection) : base(connection)
13+
private InMemoryContext(DbConnection connection, IServiceProvider serviceProvider) : base(connection, serviceProvider)
1314
{ }
1415

15-
public static new InMemoryContext Create(DbConnection connection)
16-
=> Initialize(new InMemoryContext(connection));
16+
public static new InMemoryContext Create(DbConnection connection, IServiceCollection services = null)
17+
{
18+
services = ConfigureDbServices(services);
19+
return Initialize(new InMemoryContext(connection, services.BuildServiceProvider()));
20+
}
21+
22+
public static IServiceCollection ConfigureDbServices(IServiceCollection services = null)
23+
{
24+
services ??= new ServiceCollection();
25+
services.Configure<IdentityOptions>(options =>
26+
{
27+
options.Stores.SchemaVersion = IdentitySchemaVersions.Version3;
28+
});
29+
return services;
30+
}
1731

1832
public static TContext Initialize<TContext>(TContext context) where TContext : DbContext
1933
{
@@ -28,17 +42,25 @@ public class InMemoryContext<TUser> :
2842
where TUser : IdentityUser
2943
{
3044
private readonly DbConnection _connection;
45+
private readonly IServiceProvider _serviceProvider;
3146

32-
private InMemoryContext(DbConnection connection)
47+
private InMemoryContext(DbConnection connection, IServiceProvider serviceProvider)
3348
{
3449
_connection = connection;
50+
_serviceProvider = serviceProvider;
3551
}
3652

37-
public static InMemoryContext<TUser> Create(DbConnection connection)
38-
=> InMemoryContext.Initialize(new InMemoryContext<TUser>(connection));
53+
public static InMemoryContext<TUser> Create(DbConnection connection, IServiceCollection services = null)
54+
{
55+
services = InMemoryContext.ConfigureDbServices(services);
56+
return InMemoryContext.Initialize(new InMemoryContext<TUser>(connection, services.BuildServiceProvider()));
57+
}
3958

4059
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
41-
=> optionsBuilder.UseSqlite(_connection);
60+
{
61+
optionsBuilder.UseSqlite(_connection);
62+
optionsBuilder.UseApplicationServiceProvider(_serviceProvider);
63+
}
4264
}
4365

4466
public class InMemoryContext<TUser, TRole, TKey> : IdentityDbContext<TUser, TRole, TKey>
@@ -47,17 +69,25 @@ public class InMemoryContext<TUser, TRole, TKey> : IdentityDbContext<TUser, TRol
4769
where TKey : IEquatable<TKey>
4870
{
4971
private readonly DbConnection _connection;
72+
private readonly IServiceProvider _serviceProvider;
5073

51-
protected InMemoryContext(DbConnection connection)
74+
protected InMemoryContext(DbConnection connection, IServiceProvider serviceProvider)
5275
{
5376
_connection = connection;
77+
_serviceProvider = serviceProvider;
5478
}
5579

56-
public static InMemoryContext<TUser, TRole, TKey> Create(DbConnection connection)
57-
=> InMemoryContext.Initialize(new InMemoryContext<TUser, TRole, TKey>(connection));
80+
public static InMemoryContext<TUser, TRole, TKey> Create(DbConnection connection, IServiceCollection services = null)
81+
{
82+
services = InMemoryContext.ConfigureDbServices(services);
83+
return InMemoryContext.Initialize(new InMemoryContext<TUser, TRole, TKey>(connection, services.BuildServiceProvider()));
84+
}
5885

5986
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
60-
=> optionsBuilder.UseSqlite(_connection);
87+
{
88+
optionsBuilder.UseSqlite(_connection);
89+
optionsBuilder.UseApplicationServiceProvider(_serviceProvider);
90+
}
6191
}
6292

6393
public abstract class InMemoryContext<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken> :

src/Identity/EntityFrameworkCore/test/EF.InMemory.Test/InMemoryStoreWithGenericsTest.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ public InMemoryEFUserStoreTestWithGenerics(InMemoryDatabaseFixture fixture)
2424

2525
var services = new ServiceCollection();
2626
services.AddHttpContextAccessor();
27+
services.Configure<IdentityOptions>(options =>
28+
{
29+
options.Stores.SchemaVersion = IdentitySchemaVersions.Version3;
30+
});
2731
services.AddDbContext<InMemoryContextWithGenerics>(
2832
options => options
2933
.UseSqlite(_fixture.Connection)

src/Identity/EntityFrameworkCore/test/EF.Test/DbUtil.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ public static IServiceCollection ConfigureDbServices<TContext>(
3030
.UseSqlite(connection);
3131
});
3232

33+
services.Configure<IdentityOptions>(options =>
34+
{
35+
options.Stores.SchemaVersion = IdentitySchemaVersions.Version3;
36+
});
37+
3338
return services;
3439
}
3540

src/Identity/EntityFrameworkCore/test/EF.Test/SqlStoreTestBase.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ protected virtual void SetupAddIdentity(IServiceCollection services)
3737
options.Password.RequireNonAlphanumeric = false;
3838
options.Password.RequireUppercase = false;
3939
options.User.AllowedUserNameCharacters = null;
40+
options.Stores.SchemaVersion = IdentitySchemaVersions.Version3;
4041
})
4142
.AddRoles<TRole>()
4243
.AddDefaultTokenProviders()

src/Identity/EntityFrameworkCore/test/EF.Test/UserStoreEncryptPersonalDataTest.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ protected override void SetupAddIdentity(IServiceCollection services)
2525
options.Password.RequireNonAlphanumeric = false;
2626
options.Password.RequireUppercase = false;
2727
options.User.AllowedUserNameCharacters = null;
28+
options.Stores.SchemaVersion = IdentitySchemaVersions.Version3;
2829
})
2930
.AddDefaultTokenProviders()
3031
.AddEntityFrameworkStores<TestDbContext>()

src/Identity/EntityFrameworkCore/test/EF.Test/UserStoreTest.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,6 @@ public async Task FindByEmailThrowsWithTwoUsersWithSameEmail()
195195
userB.Email = "[email protected]";
196196
IdentityResultAssert.IsSuccess(await manager.CreateAsync(userB, "password"));
197197
await Assert.ThrowsAsync<InvalidOperationException>(async () => await manager.FindByEmailAsync("[email protected]"));
198-
199198
}
200199

201200
[ConditionalFact]

src/Identity/EntityFrameworkCore/test/EF.Test/Utilities/ScratchDatabaseFixture.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Data.Common;
55
using Microsoft.Data.Sqlite;
66
using Microsoft.EntityFrameworkCore;
7+
using Microsoft.Extensions.DependencyInjection;
78

89
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test;
910

@@ -23,7 +24,14 @@ public ScratchDatabaseFixture()
2324
}
2425

2526
private DbContext CreateEmptyContext()
26-
=> new DbContext(new DbContextOptionsBuilder().UseSqlite(_connection).Options);
27+
{
28+
var services = new ServiceCollection();
29+
services.Configure<IdentityOptions>(options => options.Stores.SchemaVersion = IdentitySchemaVersions.Version3);
30+
return new DbContext(new DbContextOptionsBuilder()
31+
.UseSqlite(_connection)
32+
.UseApplicationServiceProvider(services.BuildServiceProvider())
33+
.Options);
34+
}
2735

2836
public DbConnection Connection => _connection;
2937

0 commit comments

Comments
 (0)