dotnet / dotnet/aspnetcore

Serve the well-known passkey endpoints document in Identity

Open
#68,109 2 comments 0 reactions 0 assignees View on GitHub
api-proposal api-suggestion area-identity
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 6h
Merged PRs (30d)
290

Description

## 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](https://w3c.github.io/webappsec-passkey-endpoints/) 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.

```http
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

```diff
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 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](https://github.com/w3c/webappsec-passkey-endpoints/issues/15), hostname restrictions [w3c/webappsec-passkey-endpoints#18](https://github.com/w3c/webappsec-passkey-endpoints/issues/18), and redirects [w3c/webappsec-passkey-endpoints#2](https://github.com/w3c/webappsec-passkey-endpoints/issues/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:

```csharp
#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:

```csharp
builder.Services.AddPasskeyEndpoints(options =>
options.Enroll = "https://app.contoso.com/settings/passkeys");
```

Because it returns `IEndpointConventionBuilder`, the usual conventions compose:

```csharp
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.

Contributor guide

Open the contributing guide

Research direction

Start with the proposed AddPasskeyEndpoints and MapWellKnownPasskeyEndpoints entry points, then inspect IdentitySample.PasskeyUI wiring and the BlazorWeb-CSharp template context. Done means the root well-known endpoint serves the optional JSON fields with correct URL resolution and composes with endpoint conventions; verify the UsePathBase and proxy cases described.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
authentication, backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.