Skip to content

Commit d6a0582

Browse files
[release/10.0-preview7] Passkey design follow-ups (#62841)
* Passkey design follow-ups (#62530) * Use `GetService()` instead of `GetRequiredService()` * Remove usings
1 parent 536de2f commit d6a0582

File tree

76 files changed

+1849
-2195
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+1849
-2195
lines changed
Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,44 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Microsoft.AspNetCore.Http;
5+
46
namespace Microsoft.AspNetCore.Identity;
57

68
/// <summary>
7-
/// Represents a handler for passkey assertion and attestation.
9+
/// Represents a handler for generating passkey creation and request options and performing
10+
/// passkey assertion and attestation.
811
/// </summary>
912
public interface IPasskeyHandler<TUser>
1013
where TUser : class
1114
{
1215
/// <summary>
13-
/// Performs passkey attestation using the provided credential JSON and original options JSON.
16+
/// Generates passkey creation options for the specified user entity and HTTP context.
17+
/// </summary>
18+
/// <param name="userEntity">The passkey user entity for which to generate creation options.</param>
19+
/// <param name="httpContext">The HTTP context associated with the request.</param>
20+
/// <returns>A <see cref="PasskeyCreationOptionsResult"/> representing the result.</returns>
21+
Task<PasskeyCreationOptionsResult> MakeCreationOptionsAsync(PasskeyUserEntity userEntity, HttpContext httpContext);
22+
23+
/// <summary>
24+
/// Generates passkey request options for the specified user and HTTP context.
25+
/// </summary>
26+
/// <param name="user">The user for whom to generate request options.</param>
27+
/// <param name="httpContext">The HTTP context associated with the request.</param>
28+
/// <returns>A <see cref="PasskeyRequestOptionsResult"/> representing the result.</returns>
29+
Task<PasskeyRequestOptionsResult> MakeRequestOptionsAsync(TUser? user, HttpContext httpContext);
30+
31+
/// <summary>
32+
/// Performs passkey attestation using the provided <see cref="PasskeyAttestationContext"/>.
1433
/// </summary>
1534
/// <param name="context">The context containing necessary information for passkey attestation.</param>
16-
/// <returns>A task object representing the asynchronous operation containing the <see cref="PasskeyAttestationResult"/>.</returns>
17-
Task<PasskeyAttestationResult> PerformAttestationAsync(PasskeyAttestationContext<TUser> context);
35+
/// <returns>A <see cref="PasskeyAttestationResult"/> representing the result.</returns>
36+
Task<PasskeyAttestationResult> PerformAttestationAsync(PasskeyAttestationContext context);
1837

1938
/// <summary>
20-
/// Performs passkey assertion using the provided credential JSON, original options JSON, and optional user.
39+
/// Performs passkey assertion using the provided <see cref="PasskeyAssertionContext"/>.
2140
/// </summary>
2241
/// <param name="context">The context containing necessary information for passkey assertion.</param>
23-
/// <returns>A task object representing the asynchronous operation containing the <see cref="PasskeyAssertionResult{TUser}"/>.</returns>
24-
Task<PasskeyAssertionResult<TUser>> PerformAssertionAsync(PasskeyAssertionContext<TUser> context);
42+
/// <returns>A <see cref="PasskeyAssertionResult{TUser}"/> representing the result.</returns>
43+
Task<PasskeyAssertionResult<TUser>> PerformAssertionAsync(PasskeyAssertionContext context);
2544
}

src/Identity/Core/src/IdentityBuilderExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public static IdentityBuilder AddDefaultTokenProviders(this IdentityBuilder buil
4141
private static void AddSignInManagerDeps(this IdentityBuilder builder)
4242
{
4343
builder.Services.AddHttpContextAccessor();
44-
builder.Services.AddScoped(typeof(IPasskeyHandler<>).MakeGenericType(builder.UserType), typeof(DefaultPasskeyHandler<>).MakeGenericType(builder.UserType));
44+
builder.Services.AddScoped(typeof(IPasskeyHandler<>).MakeGenericType(builder.UserType), typeof(PasskeyHandler<>).MakeGenericType(builder.UserType));
4545
builder.Services.AddScoped(typeof(ISecurityStampValidator), typeof(SecurityStampValidator<>).MakeGenericType(builder.UserType));
4646
builder.Services.AddScoped(typeof(ITwoFactorSecurityStampValidator), typeof(TwoFactorSecurityStampValidator<>).MakeGenericType(builder.UserType));
4747
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<SecurityStampValidatorOptions>, PostConfigureSecurityStampValidatorOptions>());

src/Identity/Core/src/IdentityJsonSerializerContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ namespace Microsoft.AspNetCore.Identity;
1111
[JsonSerializable(typeof(PublicKeyCredentialRequestOptions))]
1212
[JsonSerializable(typeof(PublicKeyCredential<AuthenticatorAssertionResponse>))]
1313
[JsonSerializable(typeof(PublicKeyCredential<AuthenticatorAttestationResponse>))]
14+
[JsonSerializable(typeof(PasskeyAttestationState))]
15+
[JsonSerializable(typeof(PasskeyAssertionState))]
1416
[JsonSourceGenerationOptions(
1517
JsonSerializerDefaults.Web,
1618
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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;
5+
6+
/// <summary>
7+
/// Specifies options for passkey requirements.
8+
/// </summary>
9+
public class IdentityPasskeyOptions
10+
{
11+
/// <summary>
12+
/// Gets or sets the time that the browser should wait for the authenticator to provide a passkey.
13+
/// </summary>
14+
/// <remarks>
15+
/// <para>
16+
/// This option applies to both creating a new passkey and requesting an existing passkey.
17+
/// This is treated as a hint to the browser, and the browser may choose to ignore it.
18+
/// </para>
19+
/// <para>
20+
/// The default value is 5 minutes.
21+
/// </para>
22+
/// <para>
23+
/// See <see href="https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-timeout"/>
24+
/// and <see href="https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-timeout"/>.
25+
/// </para>
26+
/// </remarks>
27+
public TimeSpan AuthenticatorTimeout { get; set; } = TimeSpan.FromMinutes(5);
28+
29+
/// <summary>
30+
/// Gets or sets the size of the challenge in bytes sent to the client during attestation and assertion.
31+
/// </summary>
32+
/// <remarks>
33+
/// <para>
34+
/// This option applies to both creating a new passkey and requesting an existing passkey.
35+
/// </para>
36+
/// <para>
37+
/// The default value is 32 bytes.
38+
/// </para>
39+
/// <para>
40+
/// See <see href="https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-challenge"/>
41+
/// and <see href="https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-challenge"/>.
42+
/// </para>
43+
/// </remarks>
44+
public int ChallengeSize { get; set; } = 32;
45+
46+
/// <summary>
47+
/// Gets or sets the effective ___domain of the server.
48+
/// This should be unique and will be used as the identity for the server.
49+
/// </summary>
50+
/// <remarks>
51+
/// <para>
52+
/// This option applies to both creating a new passkey and requesting an existing passkey.
53+
/// </para>
54+
/// <para>
55+
/// If left <see langword="null"/>, the server's origin may be used instead.
56+
/// </para>
57+
/// <para>
58+
/// See <see href="https://www.w3.org/TR/webauthn-3/#rp-id"/>.
59+
/// </para>
60+
/// </remarks>
61+
public string? ServerDomain { get; set; }
62+
63+
/// <summary>
64+
/// Gets or sets the user verification requirement.
65+
/// </summary>
66+
/// <remarks>
67+
/// <para>
68+
/// This option applies to both creating a new passkey and requesting an existing passkey.
69+
/// </para>
70+
/// <para>
71+
/// Possible values are "required", "preferred", and "discouraged".
72+
/// </para>
73+
/// <para>
74+
/// If left <see langword="null"/>, the browser defaults to "preferred".
75+
/// </para>
76+
/// <para>
77+
/// See <see href="https://www.w3.org/TR/webauthn-3/#enumdef-userverificationrequirement"/>.
78+
/// </para>
79+
/// </remarks>
80+
public string? UserVerificationRequirement { get; set; }
81+
82+
/// <summary>
83+
/// Gets or sets the extent to which the server desires to create a client-side discoverable credential.
84+
/// </summary>
85+
/// <remarks>
86+
/// <para>
87+
/// This option only applies when creating a new passkey, and is not enforced on the server.
88+
/// </para>
89+
/// <para>
90+
/// Possible values are "discouraged", "preferred", or "required".
91+
/// </para>
92+
/// <para>
93+
/// If left <see langword="null"/>, the browser defaults to "preferred".
94+
/// </para>
95+
/// <para>
96+
/// See <see href="https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement"/>.
97+
/// </para>
98+
/// </remarks>
99+
public string? ResidentKeyRequirement { get; set; }
100+
101+
/// <summary>
102+
/// Gets or sets the attestation conveyance preference.
103+
/// </summary>
104+
/// <remarks>
105+
/// <para>
106+
/// This option only applies when creating a new passkey, and already-registered passkeys are not affected by it.
107+
/// To validate the attestation statement of a passkey during passkey creation, provide a value for the
108+
/// <see cref="VerifyAttestationStatement"/> option.
109+
/// </para>
110+
/// <para>
111+
/// Possible values are "none", "indirect", "direct", and "enterprise".
112+
/// </para>
113+
/// <para>
114+
/// If left <see langword="null"/>, the browser defaults to "none".
115+
/// </para>
116+
/// <para>
117+
/// See <see href="https://www.w3.org/TR/webauthn-3/#enumdef-attestationconveyancepreference"/>.
118+
/// </para>
119+
/// </remarks>
120+
public string? AttestationConveyancePreference { get; set; }
121+
122+
/// <summary>
123+
/// Gets or sets the allowed authenticator attachment.
124+
/// </summary>
125+
/// <remarks>
126+
/// <para>
127+
/// This option only applies when creating a new passkey, and already-registered passkeys are not affected by it.
128+
/// </para>
129+
/// <para>
130+
/// Possible values are "platform" and "cross-platform".
131+
/// </para>
132+
/// <para>
133+
/// If left <see langword="null"/>, any authenticator attachment modality is allowed.
134+
/// </para>
135+
/// <para>
136+
/// See <see href="https://www.w3.org/TR/webauthn-3/#enumdef-authenticatorattachment"/>.
137+
/// </para>
138+
/// </remarks>
139+
public string? AuthenticatorAttachment { get; set; }
140+
141+
/// <summary>
142+
/// Gets or sets a function that determines whether the given COSE algorithm identifier
143+
/// is allowed for passkey operations.
144+
/// </summary>
145+
/// <remarks>
146+
/// <para>
147+
/// This option only applies when creating a new passkey, and already-registered passkeys are not affected by it.
148+
/// </para>
149+
/// <para>
150+
/// If left <see langword="null"/>, all supported algorithms are allowed.
151+
/// </para>
152+
/// <para>
153+
/// See <see href="https://www.iana.org/assignments/cose/cose.xhtml#algorithms"/>.
154+
/// </para>
155+
/// </remarks>
156+
public Func<int, bool>? IsAllowedAlgorithm { get; set; }
157+
158+
/// <summary>
159+
/// Gets or sets a function that validates the origin of the request.
160+
/// </summary>
161+
/// <remarks>
162+
/// <para>
163+
/// This option applies to both creating a new passkey and requesting an existing passkey.
164+
/// </para>
165+
/// <para>
166+
/// If left <see langword="null"/>, cross-origin requests are disallowed, and the request is only
167+
/// considered valid if the request's origin header matches the credential's origin.
168+
/// </para>
169+
/// </remarks>
170+
public Func<PasskeyOriginValidationContext, ValueTask<bool>>? ValidateOrigin { get; set; }
171+
172+
/// <summary>
173+
/// Gets or sets a function that verifies the attestation statement of a passkey.
174+
/// </summary>
175+
/// <remarks>
176+
/// <para>
177+
/// This option only applies when creating a new passkey, and already-registered passkeys are not affected by it.
178+
/// </para>
179+
/// <para>
180+
/// If left <see langword="null"/>, this function does not perform any verification and always returns <see langword="true"/>.
181+
/// </para>
182+
/// </remarks>
183+
public Func<PasskeyAttestationStatementVerificationContext, ValueTask<bool>>? VerifyAttestationStatement { get; set; }
184+
}

src/Identity/Core/src/IdentityServiceCollectionExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public static class IdentityServiceCollectionExtensions
102102
services.TryAddScoped<ITwoFactorSecurityStampValidator, TwoFactorSecurityStampValidator<TUser>>();
103103
services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>();
104104
services.TryAddScoped<IUserConfirmation<TUser>, DefaultUserConfirmation<TUser>>();
105-
services.TryAddScoped<IPasskeyHandler<TUser>, DefaultPasskeyHandler<TUser>>();
105+
services.TryAddScoped<IPasskeyHandler<TUser>, PasskeyHandler<TUser>>();
106106
services.TryAddScoped<UserManager<TUser>>();
107107
services.TryAddScoped<SignInManager<TUser>>();
108108
services.TryAddScoped<RoleManager<TRole>>();

src/Identity/Core/src/PasskeyAssertionContext.cs

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,12 @@ namespace Microsoft.AspNetCore.Identity;
88
/// <summary>
99
/// Represents the context for passkey assertion.
1010
/// </summary>
11-
/// <typeparam name="TUser">The type of user associated with the passkey.</typeparam>
12-
public sealed class PasskeyAssertionContext<TUser>
13-
where TUser : class
11+
public sealed class PasskeyAssertionContext
1412
{
1513
/// <summary>
16-
/// Gets or sets the user associated with the passkey, if known.
14+
/// Gets or sets the <see cref="Http.HttpContext"/> for the current request.
1715
/// </summary>
18-
public TUser? User { get; init; }
16+
public required HttpContext HttpContext { get; init; }
1917

2018
/// <summary>
2119
/// Gets or sets the credentials obtained by JSON-serializing the result of the
@@ -24,17 +22,11 @@ public sealed class PasskeyAssertionContext<TUser>
2422
public required string CredentialJson { get; init; }
2523

2624
/// <summary>
27-
/// Gets or sets the JSON representation of the original passkey creation options provided to the browser.
28-
/// </summary>
29-
public required string OriginalOptionsJson { get; init; }
30-
31-
/// <summary>
32-
/// Gets or sets the <see cref="UserManager{TUser}"/> to retrieve user information from.
25+
/// Gets or sets the state to be used in the assertion procedure.
3326
/// </summary>
34-
public required UserManager<TUser> UserManager { get; init; }
35-
36-
/// <summary>
37-
/// Gets or sets the <see cref="HttpContext"/> for the current request.
38-
/// </summary>
39-
public required HttpContext HttpContext { get; init; }
27+
/// <remarks>
28+
/// This is expected to match the <see cref="PasskeyRequestOptionsResult.AssertionState"/>
29+
/// previously returned from <see cref="IPasskeyHandler{TUser}.MakeRequestOptionsAsync(TUser, HttpContext)"/>.
30+
/// </remarks>
31+
public required string? AssertionState { get; init; }
4032
}

src/Identity/Core/src/PasskeyAttestationContext.cs

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,25 @@ namespace Microsoft.AspNetCore.Identity;
88
/// <summary>
99
/// Represents the context for passkey attestation.
1010
/// </summary>
11-
/// <typeparam name="TUser">The type of user associated with the passkey.</typeparam>
12-
public sealed class PasskeyAttestationContext<TUser>
13-
where TUser : class
11+
public sealed class PasskeyAttestationContext
1412
{
1513
/// <summary>
16-
/// Gets or sets the credentials obtained by JSON-serializing the result of the
17-
/// <c>navigator.credentials.create()</c> JavaScript function.
18-
/// </summary>
19-
public required string CredentialJson { get; init; }
20-
21-
/// <summary>
22-
/// Gets or sets the JSON representation of the original passkey creation options provided to the browser.
14+
/// Gets or sets the <see cref="Http.HttpContext"/> for the current request.
2315
/// </summary>
24-
public required string OriginalOptionsJson { get; init; }
16+
public required HttpContext HttpContext { get; init; }
2517

2618
/// <summary>
27-
/// Gets or sets the <see cref="UserManager{TUser}"/> to retrieve user information from.
19+
/// Gets or sets the credentials obtained by JSON-serializing the result of the
20+
/// <c>navigator.credentials.create()</c> JavaScript function.
2821
/// </summary>
29-
public required UserManager<TUser> UserManager { get; init; }
22+
public required string CredentialJson { get; init; }
3023

3124
/// <summary>
32-
/// Gets or sets the <see cref="HttpContext"/> for the current request.
25+
/// Gets or sets the state to be used in the attestation procedure.
3326
/// </summary>
34-
public required HttpContext HttpContext { get; init; }
27+
/// <remarks>
28+
/// This is expected to match the <see cref="PasskeyCreationOptionsResult.AttestationState"/>
29+
/// previously returned from <see cref="IPasskeyHandler{TUser}.MakeCreationOptionsAsync(PasskeyUserEntity, HttpContext)"/>.
30+
/// </remarks>
31+
public required string? AttestationState { get; init; }
3532
}

src/Identity/Core/src/PasskeyAttestationResult.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public sealed class PasskeyAttestationResult
1414
/// Gets whether the attestation was successful.
1515
/// </summary>
1616
[MemberNotNullWhen(true, nameof(Passkey))]
17+
[MemberNotNullWhen(true, nameof(UserEntity))]
1718
[MemberNotNullWhen(false, nameof(Failure))]
1819
public bool Succeeded { get; }
1920

@@ -22,15 +23,21 @@ public sealed class PasskeyAttestationResult
2223
/// </summary>
2324
public UserPasskeyInfo? Passkey { get; }
2425

26+
/// <summary>
27+
/// Gets the user entity associated with the passkey when successful.
28+
/// </summary>
29+
public PasskeyUserEntity? UserEntity { get; }
30+
2531
/// <summary>
2632
/// Gets the error that occurred during attestation.
2733
/// </summary>
2834
public PasskeyException? Failure { get; }
2935

30-
private PasskeyAttestationResult(UserPasskeyInfo passkey)
36+
private PasskeyAttestationResult(UserPasskeyInfo passkey, PasskeyUserEntity userEntity)
3137
{
3238
Succeeded = true;
3339
Passkey = passkey;
40+
UserEntity = userEntity;
3441
}
3542

3643
private PasskeyAttestationResult(PasskeyException failure)
@@ -43,11 +50,12 @@ private PasskeyAttestationResult(PasskeyException failure)
4350
/// Creates a successful result for a passkey attestation operation.
4451
/// </summary>
4552
/// <param name="passkey">The passkey information associated with the attestation.</param>
53+
/// <param name="userEntity">The user entity associated with the attestation.</param>
4654
/// <returns>A <see cref="PasskeyAttestationResult"/> instance representing a successful attestation.</returns>
47-
public static PasskeyAttestationResult Success(UserPasskeyInfo passkey)
55+
public static PasskeyAttestationResult Success(UserPasskeyInfo passkey, PasskeyUserEntity userEntity)
4856
{
4957
ArgumentNullException.ThrowIfNull(passkey);
50-
return new PasskeyAttestationResult(passkey);
58+
return new PasskeyAttestationResult(passkey, userEntity);
5159
}
5260

5361
/// <summary>

0 commit comments

Comments
 (0)