Description
Passkeys in ASP.NET Core Identity
Proposes new APIs for passkey support in ASP.NET Core Identity.
Background and Motivation
Passkeys are a modern, phishing-resistant authentication method based on the WebAuthn and FIDO2 standards. They provide a significant security improvement over traditional passwords by relying on public key cryptography and device-based authentication. In addition to enhancing security, passkeys offer a more seamless and user-friendly sign-in experience.
There is growing industry momentum behind passkeys as a replacement for passwords. Major platforms and browsers have adopted support, and user expectations are shifting accordingly. Customers building web applications with ASP.NET Core have expressed strong interest in out-of-the-box support for passkey-based authentication (#53467).
To address this, we intend to add passkey support to the ASP.NET Core Web project templates and first-class support for passkeys in ASP.NET Core Identity.
The APIs proposed in this issue are deliberately scoped to serve ASP.NET Core Identity scenarios. They are not intended to provide general-purpose WebAuthn or FIDO2 functionality. Developers who need broader or lower-level support are encouraged to continue using existing community libraries that provide full access to the WebAuthn specification. Our goal is to provide a focused and well-integrated solution that covers the most common use cases for passkey-based registration and login in ASP.NET Core applications using Identity.
Proposed API
Implemented in #62112
Note
Some XML docs have been omitted for brevity
Microsoft.AspNetCore.Identity
Expand to view
namespace Microsoft.AspNetCore.Identity;
// Existing type. Only new members shown.
public class SignInManager<TUser>
where TUser : class
{
/// <summary>
/// Creates a new instance of <see cref="SignInManager{TUser}"/>.
/// </summary>
/// <param name="userManager">An instance of <see cref="UserManager"/> used to retrieve users from and persist users.</param>
/// <param name="contextAccessor">The accessor used to access the <see cref="HttpContext"/>.</param>
/// <param name="claimsFactory">The factory to use to create claims principals for a user.</param>
/// <param name="optionsAccessor">The accessor used to access the <see cref="IdentityOptions"/>.</param>
/// <param name="logger">The logger used to log messages, warnings and errors.</param>
/// <param name="schemes">The scheme provider that is used enumerate the authentication schemes.</param>
/// <param name="confirmation">The <see cref="IUserConfirmation{TUser}"/> used check whether a user account is confirmed.</param>
/// <param name="passkeyHandler">The <see cref="IPasskeyHandler{TUser}"/> used when performing passkey attestation and assertion.</param>
public SignInManager(
UserManager<TUser> userManager,
IHttpContextAccessor contextAccessor,
IUserClaimsPrincipalFactory<TUser> claimsFactory,
IOptions<IdentityOptions> optionsAccessor,
ILogger<SignInManager<TUser>> logger,
IAuthenticationSchemeProvider schemes,
IUserConfirmation<TUser> confirmation,
IPasskeyHandler<TUser> passkeyHandler);
/// <summary>
/// Performs passkey attestation for the given <paramref name="credentialJson"/> and <paramref name="options"/>.
/// </summary>
/// <param name="credentialJson">The credentials obtained by JSON-serializing the result of the <c>navigator.credentials.create()</c> JavaScript function.</param>
/// <param name="options">The original passkey creation options provided to the browser.</param>
/// <returns>
/// A task object representing the asynchronous operation containing the <see cref="PasskeyAttestationResult"/>.
/// </returns>
public virtual async Task<PasskeyAttestationResult> PerformPasskeyAttestationAsync(string credentialJson, PasskeyCreationOptions options);
/// <summary>
/// Performs passkey assertion for the given <paramref name="credentialJson"/> and <paramref name="options"/>.
/// </summary>
/// <param name="credentialJson">The credentials obtained by JSON-serializing the result of the <c>navigator.credentials.get()</c> JavaScript function.</param>
/// <param name="options">The original passkey creation options provided to the browser.</param>
/// <returns>
/// A task object representing the asynchronous operation containing the <see cref="PasskeyAssertionResult{TUser}"/>.
/// </returns>
public virtual async Task<PasskeyAssertionResult<TUser>> PerformPasskeyAssertionAsync(string credentialJson, PasskeyRequestOptions options);
/// <summary>
/// Attempts to sign in the user with a passkey.
/// </summary>
/// <param name="credentialJson">The credentials obtained by JSON-serializing the result of the <c>navigator.credentials.get()</c> JavaScript function.</param>
/// <param name="options">The original passkey request options provided to the browser.</param>
/// <returns>
/// The task object representing the asynchronous operation containing the <see cref="SignInResult"/>
/// for the sign-in attempt.
/// </returns>
public virtual async Task<SignInResult> PasskeySignInAsync(string credentialJson, PasskeyRequestOptions options);
/// <summary>
/// Generates a <see cref="PasskeyCreationOptions"/> and stores it in the current <see cref="HttpContext"/> for later retrieval.
/// </summary>
/// <param name="creationArgs">Args for configuring the <see cref="PasskeyCreationOptions"/>.</param>
/// <returns>
/// A task object representing the asynchronous operation containing the <see cref="PasskeyCreationOptions"/>.
/// </returns>
public virtual async Task<PasskeyCreationOptions> ConfigurePasskeyCreationOptionsAsync(PasskeyCreationArgs creationArgs);
/// <summary>
/// Generates a <see cref="PasskeyRequestOptions"/> and stores it in the current <see cref="HttpContext"/> for later retrieval.
/// </summary>
/// <param name="requestArgs">Args for configuring the <see cref="PasskeyRequestOptions"/>.</param>
/// <returns>
/// A task object representing the asynchronous operation containing the <see cref="PasskeyRequestOptions"/>.
/// </returns>
public virtual async Task<PasskeyRequestOptions> ConfigurePasskeyRequestOptionsAsync(PasskeyRequestArgs<TUser> requestArgs);
/// <summary>
/// Generates a <see cref="PasskeyCreationOptions"/> to create a new passkey for a user.
/// </summary>
/// <param name="creationArgs">Args for configuring the <see cref="PasskeyCreationOptions"/>.</param>
/// <returns>
/// A task object representing the asynchronous operation containing the <see cref="PasskeyCreationOptions"/>.
/// </returns>
public virtual async Task<PasskeyCreationOptions> GeneratePasskeyCreationOptionsAsync(PasskeyCreationArgs creationArgs);
/// <summary>
/// Generates a <see cref="PasskeyRequestOptions"/> to request an existing passkey for a user.
/// </summary>
/// <param name="requestArgs">Args for configuring the <see cref="PasskeyRequestOptions"/>.</param>
/// <returns>
/// A task object representing the asynchronous operation containing the <see cref="PasskeyRequestOptions"/>.
/// </returns>
public virtual async Task<PasskeyRequestOptions> GeneratePasskeyRequestOptionsAsync(PasskeyRequestArgs<TUser>? requestArgs);
/// <summary>
/// Retrieves the <see cref="PasskeyCreationOptions"/> stored in the current <see cref="HttpContext"/>.
/// </summary>
/// <returns>
/// A task object representing the asynchronous operation containing the <see cref="PasskeyCreationOptions"/>.
/// </returns>
public virtual async Task<PasskeyCreationOptions?> RetrievePasskeyCreationOptionsAsync();
/// <summary>
/// Retrieves the <see cref="PasskeyRequestOptions"/> stored in the current <see cref="HttpContext"/>.
/// </summary>
/// <returns>
/// A task object representing the asynchronous operation containing the <see cref="PasskeyRequestOptions"/>.
/// </returns>
public virtual async Task<PasskeyRequestOptions?> RetrievePasskeyRequestOptionsAsync();
}
/// <summary>
/// Represents arguments for generating <see cref="PasskeyCreationOptions"/>.
/// </summary>
public sealed class PasskeyCreationArgs
{
/// <summary>
/// Constructs a new <see cref="PasskeyCreationArgs">.
/// </summary>
/// <param name="userEntity">The user entity to be associated with the passkey.</param>
public PasskeyCreationArgs(PasskeyUserEntity userEntity);
public PasskeyUserEntity UserEntity { get; }
public AuthenticatorSelectionCriteria? AuthenticatorSelection { get; set; }
public string Attestation { get; set; } = "none";
public JsonElement? Extensions { get; set; }
}
/// <summary>
/// Represents options for creating a passkey.
/// </summary>
/// <remarks>
/// See <see href="https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialcreationoptions"/>.
/// </remarks>
public sealed class PasskeyCreationOptions
{
/// <summary>
/// Constructs a new <see cref="PasskeyCreationOptions">.
/// </summary>
/// <param name="userEntity">The user entity associated with the passkey.</param>
/// <param name="optionsJson">The JSON representation of the options.</param>
public PasskeyCreationOptions(PasskeyUserEntity userEntity, string optionsJson);
public PasskeyUserEntity UserEntity { get; } = userEntity;
/// <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();
// Same XML docs and implementation as AsJson()
public override string ToString();
}
/// <summary>
/// Represents arguments for generating <see cref="PasskeyRequestOptions"/>.
/// </summary>
public sealed class PasskeyRequestArgs<TUser>
where TUser : class
{
/// <summary>
/// Gets or sets the user verification requirement.
/// </summary>
/// <remarks>
/// See <see href="https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-userverification"/>.
/// Possible values are "required", "preferred", and "discouraged".
/// The default value is "preferred".
/// </remarks>
public string UserVerification { get; set; } = "preferred";
/// <summary>
/// Gets or sets the user to be authenticated.
/// </summary>
/// <remarks>
/// While this value is optional, it should be specified if the authenticating
/// user can be identified. This can happen if, for example, the user provides
/// a username before signing in with a passkey.
/// </remarks>
public TUser? User { get; set; }
public JsonElement? Extensions { get; set; }
}
/// <summary>
/// Represents options for a passkey request.
/// </summary>
/// <remarks>
/// See <see href="https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions"/>.
/// </remarks>
public sealed class PasskeyRequestOptions
{
/// <summary>
/// Constructs a new <see cref="PasskeyRequestOptions"/>.
/// </summary>
/// <param name="userId">The ID of the user for whom this request is made.</param>
/// <param name="optionsJson">The JSON representation of the options.</param>
public PasskeyRequestOptions(string? userId, string optionsJson);
public string? UserId { get; } = userId;
/// <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-publickeycredentialrequestoptionsjson"/>.
/// </remarks>
public string AsJson();
// Same XML docs and implementation as AsJson()
public override string ToString();
}
/// <summary>
/// Represents information about the user associated with a passkey.
/// </summary>
public sealed class PasskeyUserEntity
{
/// <summary>
/// Constructs a new <see cref="PasskeyUserEntity">.
/// </summary>
/// <param name="id">The user ID.</param>
/// <param name="name">The name of the user.</param>
/// <param name="displayName">The display name of the user. When omitted, defaults to <paramref name="name"/>.</param>
public PasskeyUserEntity(string id, string name, string? displayName);
public string Id { get; }
public string Name { get; }
public string DisplayName { get; }
}
/// <summary>
/// Represents a handler for passkey assertion and attestation.
/// </summary>
public interface IPasskeyHandler<TUser>
where TUser : class
{
/// <summary>
/// Performs passkey attestation using the provided credential JSON and original options JSON.
/// </summary>
/// <param name="context">The context containing necessary information for passkey attestation.</param>
/// <returns>A task object representing the asynchronous operation containing the <see cref="PasskeyAttestationResult"/>.</returns>
Task<PasskeyAttestationResult> PerformAttestationAsync(PasskeyAttestationContext<TUser> context);
/// <summary>
/// Performs passkey assertion using the provided credential JSON, original options JSON, and optional user.
/// </summary>
/// <param name="context">The context containing necessary information for passkey assertion.</param>
/// <returns>A task object representing the asynchronous operation containing the <see cref="PasskeyAssertionResult{TUser}"/>.</returns>
Task<PasskeyAssertionResult<TUser>> PerformAssertionAsync(PasskeyAssertionContext<TUser> context);
}
/// <summary>
/// Represents the context for passkey attestation.
/// </summary>
/// <typeparam name="TUser">The type of user associated with the passkey.</typeparam>
public sealed class PasskeyAttestationContext<TUser>
where TUser : class
{
/// <summary>
/// Gets or sets the credentials obtained by JSON-serializing the result of the
/// <c>navigator.credentials.create()</c> JavaScript function.
/// </summary>
public required string CredentialJson { get; init; }
/// <summary>
/// Gets or sets the JSON representation of the original passkey creation options provided to the browser.
/// </summary>
public required string OriginalOptionsJson { get; init; }
/// <summary>
/// Gets or sets the <see cref="UserManager{TUser}"/> to retrieve user information from.
/// </summary>
public required UserManager<TUser> UserManager { get; init; }
/// <summary>
/// Gets or sets the <see cref="HttpContext"/> for the current request.
/// </summary>
public required HttpContext HttpContext { get; init; }
}
/// <summary>
/// Represents the context for passkey assertion.
/// </summary>
/// <typeparam name="TUser">The type of user associated with the passkey.</typeparam>
public sealed class PasskeyAssertionContext<TUser>
where TUser : class
{
/// <summary>
/// Gets or sets the user associated with the passkey, if known.
/// </summary>
public TUser? User { get; init; }
/// <summary>
/// Gets or sets the credentials obtained by JSON-serializing the result of the
/// <c>navigator.credentials.get()</c> JavaScript function.
/// </summary>
public required string CredentialJson { get; init; }
/// <summary>
/// Gets or sets the JSON representation of the original passkey creation options provided to the browser.
/// </summary>
public required string OriginalOptionsJson { get; init; }
/// <summary>
/// Gets or sets the <see cref="UserManager{TUser}"/> to retrieve user information from.
/// </summary>
public required UserManager<TUser> UserManager { get; init; }
/// <summary>
/// Gets or sets the <see cref="HttpContext"/> for the current request.
/// </summary>
public required HttpContext HttpContext { get; init; }
}
/// <summary>
/// The default passkey handler.
/// </summary>
public sealed partial class DefaultPasskeyHandler<TUser> : IPasskeyHandler<TUser>
where TUser : class
{
public DefaultPasskeyHandler(IOptions<IdentityOptions> options);
public Task<PasskeyAttestationResult> PerformAttestationAsync(PasskeyAttestationContext<TUser> context);
public Task<PasskeyAssertionResult<TUser>> PerformAssertionAsync(PasskeyAssertionContext<TUser> context);
protected virtual Task<PasskeyAttestationResult> PerformAttestationCoreAsync(PasskeyAttestationContext<TUser> context);
protected virtual Task<PasskeyAssertionResult<TUser>> PerformAssertionCoreAsync(PasskeyAssertionContext<TUser> context);
protected virtual Task<bool> IsValidOriginAsync(PasskeyOriginInfo originInfo, HttpContext httpContext);
protected virtual Task<bool> VerifyAttestationStatementAsync(ReadOnlyMemory<byte> attestationObject, ReadOnlyMemory<byte> clientDataHash, HttpContext httpContext);
}
/// <summary>
/// Contains information used for determining whether a passkey's origin is valid.
/// </summary>
public readonly struct PasskeyOriginInfo
{
/// <summary>
/// Constructs a new <see cref="PasskeyOriginInfo"/>.
/// </summary>
/// <param name="origin">The fully-qualified origin of the requester.</param>
/// <param name="crossOrigin">Whether the request came from a cross-origin <c><iframe></c></param>
public PasskeyOriginInfo(string origin, bool? crossOrigin);
/// <summary>
/// Gets the fully-qualified origin of the requester.
/// </summary>
public string Origin { get; }
/// <summary>
/// Gets whether the request came from a cross-origin <c><iframe></c>.
/// </summary>
public bool CrossOrigin { get; }
}
/// <summary>
/// Represents an error that occurred during passkey attestation or assertion.
/// </summary>
public sealed class PasskeyException : Exception
{
public PasskeyException(string message);
public PasskeyException(string message, Exception? innerException);
}
/// <summary>
/// Represents the result of a passkey attestation operation.
/// </summary>
public sealed class PasskeyAttestationResult
{
[MemberNotNullWhen(true, nameof(Passkey))]
[MemberNotNullWhen(false, nameof(Failure))]
public bool Succeeded { get; }
public UserPasskeyInfo? Passkey { get; }
public PasskeyException? Failure { get; }
public static PasskeyAttestationResult Success(UserPasskeyInfo passkey);
public static PasskeyAttestationResult Fail(PasskeyException failure);
}
/// <summary>
/// Represents the result of a passkey assertion operation.
/// </summary>
public sealed class PasskeyAssertionResult<TUser>
where TUser : class
{
[MemberNotNullWhen(true, nameof(Passkey))]
[MemberNotNullWhen(true, nameof(User))]
[MemberNotNullWhen(false, nameof(Failure))]
public bool Succeeded { get; }
public UserPasskeyInfo? Passkey { get; }
public TUser? User { get; }
public PasskeyException? Failure { get; }
}
/// <summary>
/// A factory class for creating instances of <see cref="PasskeyAssertionResult{TUser}"/>.
/// </summary>
public static class PasskeyAssertionResult
{
public static PasskeyAssertionResult<TUser> Success<TUser>(UserPasskeyInfo passkey, TUser user)
where TUser : class;
public static PasskeyAssertionResult<TUser> Fail<TUser>(PasskeyException failure)
where TUser : class;
}
Microsoft.Extensions.Identity.Core
Expand to view
namespace Microsoft.AspNetCore.Identity;
/// <summary>
/// Used to specify requirements regarding authenticator attributes.
/// </summary>
/// <remarks>
/// See <see href="https://www.w3.org/TR/webauthn-3/#dictdef-authenticatorselectioncriteria"/>.
/// </remarks>
public sealed class AuthenticatorSelectionCriteria
{
/// <summary>
/// Gets or sets the authenticator attachment.
/// </summary>
/// <remarks>
/// See <see href="https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-authenticatorattachment"/>.
/// </remarks>
public string? AuthenticatorAttachment { get; set; }
/// <summary>
/// Gets or sets the extent to which the server desires to create a client-side discoverable credential.
/// Supported values are "discouraged", "preferred", or "required".
/// </summary>
/// <remarks>
/// See <see href="https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-residentkey"/>
/// </remarks>
public string? ResidentKey { get; set; }
/// <summary>
/// Gets whether a resident key is required.
/// </summary>
/// <remarks>
/// See <see href="https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-requireresidentkey"/>.
/// </remarks>
public bool RequireResidentKey { get; }
/// <summary>
/// Gets or sets the user verification requirement.
/// </summary>
/// <remarks>
/// See <see href="https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-userverification"/>.
/// </remarks>
public string UserVerification { get; set; } = "preferred";
}
public class IdentityOptions
{
+ public PasskeyOptions Passkey { get; set; }
}
/// <summary>
/// Specifies options for passkey requirements.
/// </summary>
public class PasskeyOptions
{
/// <summary>
/// Gets or sets the time that the server is willing to wait for a passkey operation to complete.
/// </summary>
/// <remarks>
/// The default value is 1 minute.
/// See <see href="https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-timeout"/>
/// and <see href="https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-timeout"/>.
/// </remarks>
public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(1);
/// <summary>
/// The size of the challenge in bytes sent to the client during WebAuthn attestation and assertion.
/// </summary>
/// <remarks>
/// The default value is 16 bytes.
/// See <see href="https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-challenge"/>
/// and <see href="https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-challenge"/>.
/// </remarks>
public int ChallengeSize { get; set; } = 16;
/// <summary>
/// The effective domain of the server. Should be unique and will be used as the identity for the server.
/// </summary>
/// <remarks>
/// If left <see langword="null"/>, the server's origin may be used instead.
/// See <see href="https://www.w3.org/TR/webauthn-3/#rp-id"/>.
/// </remarks>
public string? ServerDomain { get; set; }
/// <summary>
/// Gets or sets the allowed origins for credential registration and assertion.
/// When specified, these origins are explicitly allowed in addition to any origins allowed by other settings.
/// </summary>
public IList<string> AllowedOrigins { get; set; } = [];
/// <summary>
/// Gets or sets whether the current server's origin should be allowed for credentials.
/// When true, the origin of the current request will be automatically allowed.
/// </summary>
/// <remarks>
/// The default value is <see langword="true"/>.
/// </remarks>
public bool AllowCurrentOrigin { get; set; } = true;
/// <summary>
/// Gets or sets whether credentials from cross-origin iframes should be allowed.
/// </summary>
/// <remarks>
/// The default value is <see langword="false"/>.
/// </remarks>
public bool AllowCrossOriginIframes { get; set; }
/// <summary>
/// Whether or not to accept a backup eligible credential.
/// </summary>
/// <remarks>
/// The default value is <see cref="CredentialBackupPolicy.Allowed"/>.
/// </remarks>
public CredentialBackupPolicy BackupEligibleCredentialPolicy { get; set; } = CredentialBackupPolicy.Allowed;
/// <summary>
/// Whether or not to accept a backed up credential.
/// </summary>
/// <remarks>
/// The default value is <see cref="CredentialBackupPolicy.Allowed"/>.
/// </remarks>
public CredentialBackupPolicy BackedUpCredentialPolicy { get; set; } = CredentialBackupPolicy.Allowed;
/// <summary>
/// Represents the policy for credential backup eligibility and backup status.
/// </summary>
public enum CredentialBackupPolicy
{
/// <summary>
/// Indicates that the credential backup eligibility or backup status is required.
/// </summary>
Required = 0,
/// <summary>
/// Indicates that the credential backup eligibility or backup status is allowed, but not required.
/// </summary>
Allowed = 1,
/// <summary>
/// Indicates that the credential backup eligibility or backup status is disallowed.
/// </summary>
Disallowed = 2,
}
}
/// <summary>
/// Provides an abstraction for storing passkey credentials for a user.
/// </summary>
/// <typeparam name="TUser">The type that represents a user.</typeparam>
public interface IUserPasskeyStore<TUser> : IUserStore<TUser>
where TUser : class
{
/// <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);
/// <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>
Task<IList<UserPasskeyInfo>> GetPasskeysAsync(TUser user, CancellationToken cancellationToken);
/// <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>
Task<TUser?> FindByPasskeyIdAsync(byte[] 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>
Task<UserPasskeyInfo?> FindPasskeyAsync(TUser user, byte[] credentialId, CancellationToken cancellationToken);
/// <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>
Task RemovePasskeyAsync(TUser user, byte[] credentialId, CancellationToken cancellationToken);
}
/// <summary>
/// Provides information for a user's passkey credential.
/// </summary>
public class UserPasskeyInfo
{
/// <summary>
/// Initializes a new instance of <see cref="UserPasskeyInfo"/>.
/// </summary>
/// <param name="credentialId">The credential ID for the passkey.</param>
/// <param name="publicKey">The public key for the passkey.</param>
/// <param name="name">The friendly name for the passkey.</param>
/// <param name="createdAt">The time when the passkey was created.</param>
/// <param name="signCount">The signature counter for the passkey.</param>
/// <param name="transports">The transports supported by this passkey.</param>
/// <param name="isUserVerified">Indicates if the passkey has a verified user.</param>
/// <param name="isBackupEligible">Indicates if the passkey is eligible for backup.</param>
/// <param name="isBackedUp">Indicates if the passkey is currently backed up.</param>
/// <param name="attestationObject">The passkey's attestation object.</param>
/// <param name="clientDataJson">The passkey's client data JSON.</param>
public UserPasskeyInfo(
byte[] credentialId,
byte[] publicKey,
string? name,
DateTimeOffset createdAt,
uint signCount,
string[]? transports,
bool isUserVerified,
bool isBackupEligible,
bool isBackedUp,
byte[] attestationObject,
byte[] clientDataJson);
public byte[] CredentialId { get; }
public byte[] PublicKey { get; }
public string? Name { get; set; }
public DateTimeOffset CreatedAt { get; }
public uint SignCount { get; set; }
public string[]? Transports { get; }
public bool IsUserVerified { get; set; }
public bool IsBackupEligible { get; }
public bool IsBackedUp { get; set; }
public byte[] AttestationObject { get; }
public byte[] ClientDataJson { get; }
}
public static class IdentitySchemaVersions
{
+ /// <summary>
+ /// Represents the 3.0 version of the identity schema
+ /// </summary>
+ public static readonly Version Version3 = new Version(3, 0);
}
public class UserManager<TUser> : IDisposable
where TUser : class
{
+ public virtual bool SupportsUserPasskey { get; }
+ /// <summary>
+ /// Adds a new passkey for the given user or updates an existing one.
+ /// </summary>
+ /// <param name="user">The user for whom the passkey should be added or updated.</param>
+ /// <param name="passkey">The passkey to add or update.</param>
+ /// <returns>Whether the passkey was successfully set.</returns>
+ public virtual async Task<IdentityResult> SetPasskeyAsync(TUser user, UserPasskeyInfo passkey);
+ /// <summary>
+ /// Gets a user's passkeys.
+ /// </summary>
+ /// <param name="user">The user whose passkeys should be retrieved.</param>
+ /// <returns>A list of the user's passkeys.</returns>
+ public virtual Task<IList<UserPasskeyInfo>> GetPasskeysAsync(TUser user);
+ /// <summary>
+ /// Finds a user's passkey by its credential id.
+ /// </summary>
+ /// <param name="user">The user whose passkey should be retrieved.</param>
+ /// <param name="credentialId">The credential ID to search for.</param>
+ /// <returns>The passkey, or <see langword="null"/> if it doesn't exist.</returns>
+ public virtual Task<UserPasskeyInfo?> GetPasskeyAsync(TUser user, byte[] credentialId);
+ /// <summary>
+ /// Finds the user associated with a passkey.
+ /// </summary>
+ /// <param name="credentialId">The credential ID to search for.</param>
+ /// <returns>The user associated with the passkey.</returns>
+ public virtual Task<TUser?> FindByPasskeyIdAsync(byte[] credentialId);
+ /// <summary>
+ /// Removes a passkey credential from a user.
+ /// </summary>
+ /// <param name="user">The user whose passkey should be removed.</param>
+ /// <param name="credentialId">The credential id of the passkey to remove.</param>
+ /// <returns>Whether the passkey was successfully removed.</returns>
+ public virtual async Task<IdentityResult> RemovePasskeyAsync(TUser user, byte[] credentialId);
}
Microsoft.Extensions.Identity.Stores
Expand to view
namespace Microsoft.AspNetCore.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>
/// <typeparam name="TKey">The type used for the primary key for this passkey credential.</typeparam>
public class IdentityUserPasskey<TKey>
where TKey : IEquatable<TKey>
{
public virtual TKey UserId { get; set; }
public virtual byte[] CredentialId { get; set; }
public virtual byte[] PublicKey { get; set; }
public virtual string? Name { get; set; }
public virtual DateTimeOffset CreatedAt { get; set; }
public virtual uint SignCount { get; set; }
public virtual string[]? Transports { get; set; }
public virtual bool IsUserVerified { get; set; }
public virtual bool IsBackupEligible { get; set; }
public virtual bool IsBackedUp { get; set; }
public virtual byte[] AttestationObject { get; set; }
public virtual byte[] ClientDataJson { get; set; }
}
Microsoft.AspNetCore.Identity.EntityFrameworkCore
Expand to view
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore;
-public abstract class IdentityDbContext<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken> :
- IdentityUserContext<TUser, TKey, TUserClaim, TUserLogin, TUserToken>
+public class IdentityDbContext<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken> :
+ IdentityDbContext<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken, IdentityUserPasskey<TKey>>
where TUser : IdentityUser<TKey>
where TRole : IdentityRole<TKey>
where TKey : IEquatable<TKey>
where TUserClaim : IdentityUserClaim<TKey>
where TUserRole : IdentityUserRole<TKey>
where TUserLogin : IdentityUserLogin<TKey>
where TRoleClaim : IdentityRoleClaim<TKey>
where TUserToken : IdentityUserToken<TKey>
{
+ public IdentityDbContext(DbContextOptions options);
+ protected IdentityDbContext();
}
+public abstract class IdentityDbContext<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken, TUserPasskey> :
+ IdentityUserContext<TUser, TKey, TUserClaim, TUserLogin, TUserToken, TUserPasskey>
+ where TUser : IdentityUser<TKey>
+ where TRole : IdentityRole<TKey>
+ where TKey : IEquatable<TKey>
+ where TUserClaim : IdentityUserClaim<TKey>
+ where TUserRole : IdentityUserRole<TKey>
+ where TUserLogin : IdentityUserLogin<TKey>
+ where TRoleClaim : IdentityRoleClaim<TKey>
+ where TUserToken : IdentityUserToken<TKey>
+ where TUserPasskey : IdentityUserPasskey<TKey>
+{
// Members from IdentityDbContext`8 moved here
+}
-public abstract class IdentityUserContext<TUser, TKey, TUserClaim, TUserLogin, TUserToken> :
- DbContext
+public class IdentityUserContext<TUser, TKey, TUserClaim, TUserLogin, TUserToken> :
+ IdentityUserContext<TUser, TKey, TUserClaim, TUserLogin, TUserToken, IdentityUserPasskey<TKey>>
where TUser : IdentityUser<TKey>
where TKey : IEquatable<TKey>
where TUserClaim : IdentityUserClaim<TKey>
where TUserLogin : IdentityUserLogin<TKey>
where TUserToken : IdentityUserToken<TKey>
{
+ public IdentityUserContext(DbContextOptions options);
+ protected IdentityUserContext();
}
+/// <summary>
+/// Base class for the Entity Framework database context used for identity.
+/// </summary>
+/// <typeparam name="TUser">The type of user objects.</typeparam>
+/// <typeparam name="TKey">The type of the primary key for users and roles.</typeparam>
+/// <typeparam name="TUserClaim">The type of the user claim object.</typeparam>
+/// <typeparam name="TUserLogin">The type of the user login object.</typeparam>
+/// <typeparam name="TUserToken">The type of the user token object.</typeparam>
+/// <typeparam name="TUserPasskey">The type of the user passkey object.</typeparam>
+public abstract class IdentityUserContext<TUser, TKey, TUserClaim, TUserLogin, TUserToken, TUserPasskey> : DbContext
+ where TUser : IdentityUser<TKey>
+ where TKey : IEquatable<TKey>
+ where TUserClaim : IdentityUserClaim<TKey>
+ where TUserLogin : IdentityUserLogin<TKey>
+ where TUserToken : IdentityUserToken<TKey>
+ where TUserPasskey : IdentityUserPasskey<TKey>
+{
+ /// <summary>
+ /// Gets or sets the <see cref="DbSet{TEntity}"/> of User passkeys.
+ /// </summary>
+ public virtual DbSet<TUserPasskey> UserPasskeys { get; set; }
+}
public class UserOnlyStore<TUser, TContext, TKey, TUserClaim, TUserLogin, TUserToken> :
- UserStoreBase<TUser, TKey, TUserClaim, TUserLogin, TUserToken>
- IUserLoginStore<TUser>,
- IUserClaimStore<TUser>,
- IUserPasswordStore<TUser>,
- IUserSecurityStampStore<TUser>,
- IUserEmailStore<TUser>,
- IUserLockoutStore<TUser>,
- IUserPhoneNumberStore<TUser>,
- IQueryableUserStore<TUser>,
- IUserTwoFactorStore<TUser>,
- IUserAuthenticationTokenStore<TUser>,
- IUserAuthenticatorKeyStore<TUser>,
- IUserTwoFactorRecoveryCodeStore<TUser>,
- IProtectedUserStore<TUser>
+ UserOnlyStore<TUser, TContext, TKey, TUserClaim, TUserLogin, TUserToken, IdentityUserPasskey<TKey>>
where TUser : IdentityUser<TKey>
where TContext : DbContext
where TKey : IEquatable<TKey>
where TUserClaim : IdentityUserClaim<TKey>, new()
where TUserLogin : IdentityUserLogin<TKey>, new()
where TUserToken : IdentityUserToken<TKey>, new()
{
+ public UserOnlyStore(TContext context, IdentityErrorDescriber? describer = null);
}
+public class UserOnlyStore<TUser, TContext, TKey, TUserClaim, TUserLogin, TUserToken, TUserPasskey> :
+ UserStoreBase<TUser, TKey, TUserClaim, TUserLogin, TUserToken>,
+ IUserLoginStore<TUser>,
+ IUserClaimStore<TUser>,
+ IUserPasswordStore<TUser>,
+ IUserSecurityStampStore<TUser>,
+ IUserEmailStore<TUser>,
+ IUserLockoutStore<TUser>,
+ IUserPhoneNumberStore<TUser>,
+ IQueryableUserStore<TUser>,
+ IUserTwoFactorStore<TUser>,
+ IUserAuthenticationTokenStore<TUser>,
+ IUserAuthenticatorKeyStore<TUser>,
+ IUserTwoFactorRecoveryCodeStore<TUser>,
+ IProtectedUserStore<TUser>,
+ IUserPasskeyStore<TUser>
+ where TUser : IdentityUser<TKey>
+ where TContext : DbContext
+ where TKey : IEquatable<TKey>
+ where TUserClaim : IdentityUserClaim<TKey>, new()
+ where TUserLogin : IdentityUserLogin<TKey>, new()
+ where TUserToken : IdentityUserToken<TKey>, new()
+ where TUserPasskey : IdentityUserPasskey<TKey>, new()
+{
// Members from UserOnlyStore`6 moved here
+ /// <summary>
+ /// DbSet of user passkeys.
+ /// </summary>
+ protected DbSet<TUserPasskey> UserPasskeys { get; }
+ /// <summary>
+ /// Called to create a new instance of a <see cref="IdentityUserPasskey{TKey}"/>.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="passkey">The passkey.</param>
+ /// <returns></returns>
+ protected virtual TUserPasskey CreateUserPasskey(TUser user, UserPasskeyInfo passkey);
+ /// <summary>
+ /// Find a passkey with the specified credential id for a user.
+ /// </summary>
+ /// <param name="userId">The user's id.</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 user passkey if it exists.</returns>
+ protected virtual Task<TUserPasskey?> FindUserPasskeyAsync(TKey userId, byte[] credentialId, CancellationToken cancellationToken);
+ /// <summary>
+ /// Find a passkey with the specified credential id.
+ /// </summary>
+ /// <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 user passkey if it exists.</returns>
+ protected virtual Task<TUserPasskey?> FindUserPasskeyByIdAsync(byte[] credentialId, CancellationToken cancellationToken);
+ public virtual async Task SetPasskeyAsync(TUser user, UserPasskeyInfo passkey, CancellationToken cancellationToken);
+ public virtual async Task<IList<UserPasskeyInfo>> GetPasskeysAsync(TUser user, CancellationToken cancellationToken);
+ public virtual async Task<TUser?> FindByPasskeyIdAsync(byte[] credentialId, CancellationToken cancellationToken);
+ public virtual async Task<UserPasskeyInfo?> FindPasskeyAsync(TUser user, byte[] credentialId, CancellationToken cancellationToken);
+ public virtual async Task RemovePasskeyAsync(TUser user, byte[] credentialId, CancellationToken cancellationToken);
+}
public class UserStore<TUser, TRole, TContext, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim> :
- UserStoreBase<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim>,
- IProtectedUserStore<TUser>
+ UserStore<TUser, TRole, TContext, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim, IdentityUserPasskey<TKey>>
where TUser : IdentityUser<TKey>
where TRole : IdentityRole<TKey>
where TContext : DbContext
where TKey : IEquatable<TKey>
where TUserClaim : IdentityUserClaim<TKey>, new()
where TUserRole : IdentityUserRole<TKey>, new()
where TUserLogin : IdentityUserLogin<TKey>, new()
where TUserToken : IdentityUserToken<TKey>, new()
where TRoleClaim : IdentityRoleClaim<TKey>, new()
{
+ public UserStore(TContext context, IdentityErrorDescriber? describer = null);
}
+/// <summary>
+/// Represents a new instance of a persistence store for the specified user and role types.
+/// </summary>
+/// <typeparam name="TUser">The type representing a user.</typeparam>
+/// <typeparam name="TRole">The type representing a role.</typeparam>
+/// <typeparam name="TContext">The type of the data context class used to access the store.</typeparam>
+/// <typeparam name="TKey">The type of the primary key for a role.</typeparam>
+/// <typeparam name="TUserClaim">The type representing a claim.</typeparam>
+/// <typeparam name="TUserRole">The type representing a user role.</typeparam>
+/// <typeparam name="TUserLogin">The type representing a user external login.</typeparam>
+/// <typeparam name="TUserToken">The type representing a user token.</typeparam>
+/// <typeparam name="TRoleClaim">The type representing a role claim.</typeparam>
+/// <typeparam name="TUserPasskey">The type representing a user passkey.</typeparam>
+public class UserStore<TUser, TRole, TContext, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim, TUserPasskey> :
+ UserStoreBase<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim>,
+ IProtectedUserStore<TUser>,
+ IUserPasskeyStore<TUser>
+ where TUser : IdentityUser<TKey>
+ where TRole : IdentityRole<TKey>
+ where TContext : DbContext
+ where TKey : IEquatable<TKey>
+ where TUserClaim : IdentityUserClaim<TKey>, new()
+ where TUserRole : IdentityUserRole<TKey>, new()
+ where TUserLogin : IdentityUserLogin<TKey>, new()
+ where TUserToken : IdentityUserToken<TKey>, new()
+ where TRoleClaim : IdentityRoleClaim<TKey>, new()
+ where TUserPasskey : IdentityUserPasskey<TKey>, new()
+{
// Members from UserStore`9 moved here.
+ /// <summary>
+ /// Called to create a new instance of a <see cref="IdentityUserPasskey{TKey}"/>.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="passkey">The passkey.</param>
+ /// <returns></returns>
+ protected virtual TUserPasskey CreateUserPasskey(TUser user, UserPasskeyInfo passkey);
+ /// <summary>
+ /// Find a passkey with the specified credential id for a user.
+ /// </summary>
+ /// <param name="userId">The user's id.</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 user passkey if it exists.</returns>
+ protected virtual Task<TUserPasskey?> FindUserPasskeyAsync(TKey userId, byte[] credentialId, CancellationToken cancellationToken);
+ /// <summary>
+ /// Find a passkey with the specified credential id.
+ /// </summary>
+ /// <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 user passkey if it exists.</returns>
+ protected virtual Task<TUserPasskey?> FindUserPasskeyByIdAsync(byte[] credentialId, CancellationToken cancellationToken);
+ public virtual async Task SetPasskeyAsync(TUser user, UserPasskeyInfo passkey, CancellationToken cancellationToken)
+ public virtual async Task<IList<UserPasskeyInfo>> GetPasskeysAsync(TUser user, CancellationToken cancellationToken)
+ public virtual async Task<TUser?> FindByPasskeyIdAsync(byte[] credentialId, CancellationToken cancellationToken)
+ public virtual async Task<UserPasskeyInfo?> FindPasskeyAsync(TUser user, byte[] credentialId, CancellationToken cancellationToken)
+ public virtual async Task RemovePasskeyAsync(TUser user, byte[] credentialId, CancellationToken cancellationToken)
+}
Usage Examples
Adding a passkey to an existing user
// Pre-creation of passkey: configure passkey creation options
async Task<string> GetPasskeyCreationOptionsJsonAsync<TUser>(
TUser user,
UserManager<TUser> userManager,
SignInManager<TUser> signInManager)
where TUser : class
{
var userId = await userManager.GetUserIdAsync(user);
var userName = await userManager.GetUserNameAsync(user) ?? "User";
var userEntity = new PasskeyUserEntity(userId, userName, displayName: userName);
var creationArgs = new PasskeyCreationArgs(userEntity)
{
AuthenticatorSelection = new AuthenticatorSelectionCriteria
{
ResidentKey = "required",
UserVerification = "preferred",
},
Attestation = "none",
};
var options = await signInManager.ConfigurePasskeyCreationOptionsAsync(creationArgs);
return options.AsJson(); // To be returned to the browser
}
// Post-creation of passkey: add the passkey to the user
async Task<IdentityResult> AddPasskeyToUserAsync<TUser>(
TUser user,
UserManager<TUser> userManager,
SignInManager<TUser> signInManager,
string credentialJson) // Received from the browser
where TUser : class
{
// Some error handling omitted for brevity
var options = await signInManager.RetrievePasskeyCreationOptionsAsync()!;
var attestationResult = await signInManager.PerformPasskeyAttestationAsync(CredentialJson, options);
var setPasskeyResult = await userManager.SetPasskeyAsync(user, attestationResult.Passkey);
return setPasskeyResult;
}
Creating a passwordless user account
// Pre-creation of passkey: configure passkey creation options
async Task<string> GetPasskeyCreationOptionsJsonAsync(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
string userName,
string givenName)
where TUser : IUserWithSettableId
{
var userId = Guid.NewGuid().ToString();
var userEntity = new PasskeyUserEntity(userId, userName, displayName: givenName);
var creationArgs = new PasskeyCreationArgs(userEntity)
{
// ...
};
var options = await signInManager.ConfigurePasskeyCreationOptionsAsync(creationArgs);
return options.AsJson(); // To be returned to the browser
}
// Post-creation of passkey: create a new account
async Task<IdentityResult> CreateAccountAsync(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
string credentialJson) // Received from the browser
where TUser : class
{
// Some error handling omitted for brevity
var options = await signInManager.RetrievePasskeyCreationOptionsAsync()!;
var attestationResult = await signInManager.PerformPasskeyAttestationAsync(credentialJson, options);
var userEntity = options.UserEntity;
var user = new ApplicationUser(userName: userEntity.Name)
{
Id = userEntity.Id,
};
var createUserResult = await userManager.CreateAsync(user);
if (!createUserResult.Succeeded)
{
return createUserResult;
}
var setPasskeyResult = await userManager.SetPasskeyAsync(user, attestationResult.Passkey);
return setPasskeyResult;
}
Signing in with a passkey
// Pre-retrieval of passkey: configure passkey request options
async Task<string> GetPasskeyRequestOptionsJsonAsync<TUser>(
UserManager<TUser> userManager,
SignInManager<TUser> signInManager,
string? userName)
where TUser : class
{
// The username might be missing if the user didn't specify it.
// In that case, the browser or authenticator will prompt the user to
// select the account to sign in to.
var user = string.IsNullOrEmpty(userName) ? null : await userManager.FindByNameAsync(userName);
var requestArgs = new PasskeyRequestArgs<PocoUser>
{
User = user,
UserVerification = "required",
};
var options = await signInManager.ConfigurePasskeyRequestOptionsAsync(requestArgs);
return options.AsJson(); // To be returned to the browser
}
// Post-retrieval of passkey: sign in with the passkey
async Task<SignInResult> SignInWithPasskeyAsync<TUser>(
SignInManager<TUser> signInManager,
string credentialJson) // Received from the browser
{
// Some error handling omitted for brevity
var options = await signInManager.RetrievePasskeyRequestOptionsAsync()!;
var signInResult = await signInManager.PasskeySignInAsync(credentialJson, options);
return signInResult;
}
Alternative Designs
Some WebAuthn libraries from the community define public .NET types reflecting what's described in the WebAuthn specification. This creates a predictable API and enables convenient JSON-based communication between the browser and the server. The ASP.NET Core Identity implementation is similar, but it keeps WebAuthn-derived types internal for the following reasons:
- This greatly decreases the amount of new public API that needs to be kept in sync with changes to the WebAuthn spec
- We are free to adjust our implementation as the spec evolves with less concern of breaking customers
- This could include completely removing some types in the future and e.g., replacing them with hand-rolled JSON parsing logic
- In the future, if .NET provides a fully-featured WebAuthn library (not tied to Identity), we could easily adopt our implementation to make use of that library without having to maintain a separate set of abstractions
Because of this design decision, some APIs proposed in this issue accept and produce raw JSON strings matching schemas that browser APIs work with. It's up to the implementations of these .NET APIs to decide how to parse or generate these JSON strings.
An argument against this approach is that there's still an API here, it's just less visible. Even though this approach technically reduces the .NET public API surface, JSON payloads are just a different kind of API. However, JSON-based APIs are able to change freely without our immediate involvement, and customers are able to take advantage of the latest JSON representations if they're willing to provide an implementation that does so.
Risks
Despite efforts to minimize the public API surface, it's still quite large, as evidenced by the collapsible sections above. One potential risk is that the WebAuthn spec could change in a manner that we don't expect, requiring us to make uncomfortable API changes in the future.