-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Add passkeys to ASP.NET Core Identity #62112
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
FYI I have a PR adding metrics to identity here: #62078. Whoever merges second will need to react and add counters and tags for passkey signins. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds support for passkeys to ASP.NET Core Identity by extending the existing identity stores and sign‐in flows. Key changes include:
- Introducing new generic abstractions and methods for managing passkeys in both UserStore and UserOnlyStore.
- Updating IdentityUserContext model building to support passkeys under Identity Schema Version 3.
- Extending SignInManager with new APIs for passkey sign in and for configuring/retrieving passkey creation and request options.
Reviewed Changes
Copilot reviewed 111 out of 111 changed files in this pull request and generated 2 comments.
Show a summary per file
File | Description |
---|---|
src/Identity/EntityFrameworkCore/src/UserStore.cs | Added a new generic UserStore overload and methods to create, find, update, and remove user passkeys. |
src/Identity/EntityFrameworkCore/src/UserOnlyStore.cs | Extended the store for users without roles to support passkey operations along with checks for DB support. |
src/Identity/EntityFrameworkCore/src/IdentityUserContext.cs | Modified OnModelCreating to include a new entity for passkeys when using Identity Schema Version 3. |
src/Identity/Core/src/SignInManager.cs | Introduced new methods for passkey sign in and configuration of passkey creation/request options. |
PublicAPI.Unshipped.txt and build files | Updated the public API surface and project configuration to expose passkey-related features. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some comments to help with reviewing.
public interface IPasskeyRequestContextProvider | ||
{ | ||
/// <summary> | ||
/// Gets the current <see cref="PasskeyRequestContext"/>. | ||
/// </summary> | ||
PasskeyRequestContext Context { get; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The purpose of this abstraction is to allow passkey attestation and assertion logic to access information about the current request, like the host and origin.
The Microsoft.Extensions.Identity.Core
package doesn't reference Microsoft.AspNetCore.Http.Abstractions
, so we can't directly grab information off the current HttpContext
unless we move parts of the passkey implementation to Microsoft.AspNetCore.Identity
.
builder.Services.AddHttpContextAccessor(); | ||
builder.Services.AddScoped<IPasskeyRequestContextProvider, HttpPasskeyRequestContextProvider>(); | ||
builder.Services.AddScoped(typeof(ISecurityStampValidator), typeof(SecurityStampValidator<>).MakeGenericType(builder.UserType)); | ||
builder.Services.AddScoped(typeof(ITwoFactorSecurityStampValidator), typeof(TwoFactorSecurityStampValidator<>).MakeGenericType(builder.UserType)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This service allows passkey handlers to infer the server's origin from the ongoing request. We can't register this by default in Microsoft.Extensions.Identity.Core
because that package can't assume an HttpContext
will be available. Putting the service registration here means that it will get registered as long as the app is already calling AddSignInManager()
.
However, we might be able to eliminate the need for this abstraction completely by adjusting the layering and moving most of the passkey logic to Microsoft.AspNetCore.Identity
. The M.E.Identity.Core
package is mostly web-agnostic, but WebAuthn is very much not, so it might make sense to move things around.
public interface IPasskeyOriginValidator | ||
{ | ||
/// <summary> | ||
/// Determines whether the specified origin is valid for passkey operations. | ||
/// </summary> | ||
/// <param name="originInfo">Information about the passkey's origin.</param> | ||
/// <returns><c>true</c> if the origin is valid; otherwise, <c>false</c>.</returns> | ||
bool IsValidOrigin(PasskeyOriginInfo originInfo); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This interface exists because the meaning of a "valid origin" can vary widely between applications. See https://www.w3.org/TR/webauthn-3/#sctn-validating-origin.
/// <summary> | ||
/// Adds 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">The passkey to add.</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> | ||
Task SetPasskeyAsync(TUser user, UserPasskeyInfo passkey, CancellationToken cancellationToken); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering splitting this out into AddPasskeyAsync
and UpdatePasskeyAsync
.
/// <summary> | ||
/// Gets the JSON representation of the options. | ||
/// </summary> | ||
/// <remarks> | ||
/// The structure of the JSON string matches the description in the WebAuthn specification. | ||
/// See <see href="https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialcreationoptionsjson"/>. | ||
/// </remarks> | ||
public string AsJson() | ||
=> _optionsJson; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR currently just exposes the credential creation/request options as a JSON string rather than returning a strongly-typed object. That way we're not locked into making the entirety of the Passkeys/
folder public
. In the future, if we wanted to e.g., write a hand-crafted JSON writer that constructed a string without first building a .NET representation, we could do that.
/// <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; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These flags are all present on the authenticator data flags. We could just store the flags byte directly so that we don't introduce another schema change in the future, should we find ourselves wanting to store more flags.
async function createCredential(optionsJSON) { | ||
// See: https://www.w3.org/TR/webauthn-2/#sctn-registering-a-new-credential | ||
|
||
// 1. Let options be a new PublicKeyCredentialCreationOptions structure configured to | ||
// the Relying Party’s needs for the ceremony. | ||
// See: https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-parsecreationoptionsfromjson | ||
const options = PublicKeyCredential.parseCreationOptionsFromJSON(optionsJSON); | ||
|
||
// 2. Call navigator.credentials.create() and pass options as the publicKey option. | ||
// Let credential be the result of the successfully resolved promise. | ||
// If the promise is rejected, abort the ceremony with a user-visible error, | ||
// or otherwise guide the user experience as might be determinable from the | ||
// context available in the rejected promise. | ||
const credential = await navigator.credentials.create({ publicKey: options }); | ||
|
||
// 3. Let response be credential.response. If response is not an instance of | ||
// AuthenticatorAttestationResponse, abort the ceremony with a user-visible error. | ||
if (!(credential?.response instanceof AuthenticatorAttestationResponse)) { | ||
throw new Error('The authenticator failed to provide a valid credential.'); | ||
} | ||
|
||
// Continue the ceremony on the server. | ||
// See: https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-tojson | ||
return JSON.stringify(credential); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could move this code to a package, but it's so small that I think it's safe to include it in the template.
However, I'm considering changing the options
parameter here to represent the top-level object passed to the navigator.credentials.create()
function instead of representing the publicKey
property. That's because the WebAuthn level 3 spec references another top-level property, mediation
in the ceremony, and we might want a way to configure it (and other not-yet-introduced properties) in the future.
Also, this script assumes it's loaded as part of a full page navigation, not an enhanced navigation. This strategy currently works because the script gets rendered as a result of a non-enhanced form post.
[ | ||
new(COSEAlgorithmIdentifier.ES256), | ||
new(COSEAlgorithmIdentifier.PS256), | ||
new(COSEAlgorithmIdentifier.ES384), | ||
new(COSEAlgorithmIdentifier.PS384), | ||
new(COSEAlgorithmIdentifier.PS512), | ||
new(COSEAlgorithmIdentifier.RS256), | ||
new(COSEAlgorithmIdentifier.ES512), | ||
new(COSEAlgorithmIdentifier.RS384), | ||
new(COSEAlgorithmIdentifier.RS512), | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This list does not include RS1, even though the FIDO conformance testing tool checks for it. Maybe we should make this list configurable.
Add passkeys to ASP.NET Core Identity
This PR adds passkey support to ASP.NET Core Identity.
Description
Following is a summary of the changes in this PR:
Microsoft.AspNetCore.Identity.EntityFrameworkCore
SignInManager
andUserManager
for passkey management and sign inNote that the goal of this PR is to add support for passkey authentication in ASP.NET Core Identity. While it implements core WebAuthn functionality, it does not provide a complete and general-purpose WebAuthn/FIDO2 library. The public API surface is limited in order to enable long-term stability of the feature. Targeted extensibility points were added to enable functionality not implemented by default, most notably attestation statement validation. This allows the use of third-party libraries to fill the missing gaps, when desired. Community feedback may result in additional extensibility APIs being added in the future.
E2E tests are a work in progress. I'll add unit tests after we agree on the design to avoid churn.
Fixes #53467