Background and Motivation
Passkeys only replace passwords if users find the page that creates one, and that page is usually buried in account settings. The W3C passkey endpoints draft fixes this: a site publishes a small JSON document at /.well-known/passkey-endpoints, and a credential manager holding a saved password for that site reads it and offers "upgrade to a passkey", linking straight to the right page.
The document is fetched out of band by the credential manager itself. Nothing in the application ever requests it.
Identity gained passkey support in .NET 10 but has no way to advertise it, so apps must hand-roll the document and get the URL, escaping and anonymous access right themselves. Tracked by #67300; implemented in #68066.
GET https://contoso.com/.well-known/passkey-endpoints
HTTP/1.1 200 OK
Content-Type: application/json
{"enroll":"https://contoso.com/Account/Manage/Passkeys",
"manage":"https://contoso.com/Account/Manage/Passkeys"}
Proposed API
namespace Microsoft.AspNetCore.Identity;
+ [Experimental("ASP0033", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")]
+ public sealed class PasskeyEndpointsOptions
+ {
+ public PasskeyEndpointsOptions();
+ public string? Enroll { get; set; }
+ public string? Manage { get; set; }
+ public string? PrfUsageDetails { get; set; }
+ }
namespace Microsoft.Extensions.DependencyInjection;
+ [Experimental("ASP0033", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")]
+ public static class PasskeyEndpointsServiceCollectionExtensions
+ {
+ public static IServiceCollection AddPasskeyEndpoints(
+ this IServiceCollection services,
+ Action<PasskeyEndpointsOptions> configure);
+ }
namespace Microsoft.AspNetCore.Routing;
+ [Experimental("ASP0033", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")]
+ public static class PasskeyEndpointsEndpointRouteBuilderExtensions
+ {
+ public static IEndpointConventionBuilder MapWellKnownPasskeyEndpoints(
+ this IEndpointRouteBuilder endpoints);
+ }
The three options map one-to-one onto §3.1 of the spec. All are optional and omitted when unset; an empty document is valid and signals passkey support without advertising pages. Values may be absolute URLs or app-relative paths resolved against the request.
The surface is [Experimental] under a new diagnostic ID, ASP0033, because the semantics are still moving even though the shape is settled. Three member-level questions are open upstream: server-relative URLs w3c/webappsec-passkey-endpoints#15, hostname restrictions w3c/webappsec-passkey-endpoints#18, and redirects w3c/webappsec-passkey-endpoints#2. The middle one would reject configuration that works today. The attribute comes off when the spec reaches CR.
The default BlazorWeb-CSharp template does not wire this up, since a template that needs a #pragma to compile is not a stability promise. IdentitySample.PasskeyUI keeps the wiring with a suppression.
Usage Examples
Blazor Web App template with individual accounts, the whole thing:
#pragma warning disable ASP0033 // Experimental: passkey endpoints
builder.Services.AddPasskeyEndpoints(options =>
{
options.Enroll = "/Account/Manage/Passkeys";
options.Manage = "/Account/Manage/Passkeys";
});
var app = builder.Build();
app.MapWellKnownPasskeyEndpoints();
Headless backend whose enrollment page lives in a SPA on another origin:
builder.Services.AddPasskeyEndpoints(options =>
options.Enroll = "https://app.contoso.com/settings/passkeys");
Because it returns IEndpointConventionBuilder, the usual conventions compose:
app.MapWellKnownPasskeyEndpoints().CacheOutput().RequireHost("contoso.com");
Alternative Designs
Middleware registered by an IStartupFilter, with no Map call. This was the original implementation. The appeal: nobody in the app calls this endpoint and the spec fixes its URL, so asking the developer to place it is a step that can only be got wrong MapGroup("/api").MapWellKnownPasskeyEndpoints() compiles, boots, and serves valid JSON where no credential manager will look.
We abandoned it because the pipeline position that protects the URL destroys the URLs inside it. Startup filters wrap the application's own Configure, so the middleware necessarily ran before app.UseForwardedHeaders(). Behind a proxy:
served: {"enroll":"http://internal.local/Account/Manage/Passkeys"}
expected: {"enroll":"https://contoso.com/Account/Manage/Passkeys"}
An endpoint runs after routing, therefore after UseForwardedHeaders, and sees the real scheme and host.
Risks
- No breaking changes. All new, unshipped API, and marked
[Experimental], so the semantics can still move as the spec settles without that counting as a break. Apps that hand-rolled the document are unaffected unless they call MapWellKnownPasskeyEndpoints.
- A
Map method that throws when composed with MapGroup breaks the expectation that conventions compose uniformly. We think it's justified, the spec allows exactly one location.
UsePathBase: credential managers fetch at the origin root, which never matches the path base, so relative values resolve without the prefix. Server-supplied path bases (IIS virtual directories) work correctly. Covered by tests.
Background and Motivation
Passkeys only replace passwords if users find the page that creates one, and that page is usually buried in account settings. The W3C passkey endpoints draft fixes this: a site publishes a small JSON document at
/.well-known/passkey-endpoints, and a credential manager holding a saved password for that site reads it and offers "upgrade to a passkey", linking straight to the right page.The document is fetched out of band by the credential manager itself. Nothing in the application ever requests it.
Identity gained passkey support in .NET 10 but has no way to advertise it, so apps must hand-roll the document and get the URL, escaping and anonymous access right themselves. Tracked by #67300; implemented in #68066.
Proposed API
The three options map one-to-one onto §3.1 of the spec. All are optional and omitted when unset; an empty document is valid and signals passkey support without advertising pages. Values may be absolute URLs or app-relative paths resolved against the request.
The surface is
[Experimental]under a new diagnostic ID, ASP0033, because the semantics are still moving even though the shape is settled. Three member-level questions are open upstream: server-relative URLs w3c/webappsec-passkey-endpoints#15, hostname restrictions w3c/webappsec-passkey-endpoints#18, and redirects w3c/webappsec-passkey-endpoints#2. The middle one would reject configuration that works today. The attribute comes off when the spec reaches CR.The default
BlazorWeb-CSharptemplate does not wire this up, since a template that needs a#pragmato compile is not a stability promise.IdentitySample.PasskeyUIkeeps the wiring with a suppression.Usage Examples
Blazor Web App template with individual accounts, the whole thing:
Headless backend whose enrollment page lives in a SPA on another origin:
Because it returns
IEndpointConventionBuilder, the usual conventions compose:Alternative Designs
Middleware registered by an
IStartupFilter, with noMapcall. This was the original implementation. The appeal: nobody in the app calls this endpoint and the spec fixes its URL, so asking the developer to place it is a step that can only be got wrongMapGroup("/api").MapWellKnownPasskeyEndpoints()compiles, boots, and serves valid JSON where no credential manager will look.We abandoned it because the pipeline position that protects the URL destroys the URLs inside it. Startup filters wrap the application's own
Configure, so the middleware necessarily ran beforeapp.UseForwardedHeaders(). Behind a proxy:An endpoint runs after routing, therefore after
UseForwardedHeaders, and sees the real scheme and host.Risks
[Experimental], so the semantics can still move as the spec settles without that counting as a break. Apps that hand-rolled the document are unaffected unless they callMapWellKnownPasskeyEndpoints.Mapmethod that throws when composed withMapGroupbreaks the expectation that conventions compose uniformly. We think it's justified, the spec allows exactly one location.UsePathBase: credential managers fetch at the origin root, which never matches the path base, so relative values resolve without the prefix. Server-supplied path bases (IIS virtual directories) work correctly. Covered by tests.