Skip to content

Add passkey endpoints to MapIdentityApi - #68198

Open
rolandVi wants to merge 1 commit into
dotnet:rolandVi/passkey-workfrom
rolandVi:passkeys/identity-api-endpoints
Open

Add passkey endpoints to MapIdentityApi#68198
rolandVi wants to merge 1 commit into
dotnet:rolandVi/passkey-workfrom
rolandVi:passkeys/identity-api-endpoints

Conversation

@rolandVi

@rolandVi rolandVi commented Aug 4, 2026

Copy link
Copy Markdown
Member

API proposal in #68197.

SPA and mobile apps that use MapIdentityApi() can now register and sign in with passkeys. Today those endpoints only do passwords, so a JavaScript or MAUI client either drops passkeys or hand-writes both WebAuthn ceremonies against SignInManager itself, including the challenge state that has to survive between the two requests.

Route Auth Purpose
POST /passkeys/requestOptions anonymous assertion options for the browser
POST /passkeys/login anonymous verify the assertion and sign in
POST /manage/passkeys/creationOptions authorized attestation options for the signed-in user
POST /manage/passkeys authorized verify the attestation and store the passkey

Example usage

const options = await fetch('/identity/passkeys/requestOptions', {
  method: 'POST',
  credentials: 'include',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email }),
}).then(r => r.json());

const credential = await navigator.credentials.get({
  publicKey: PublicKeyCredential.parseRequestOptionsFromJSON(options),
});

await fetch('/identity/passkeys/login?useCookies=true', {
  method: 'POST',
  credentials: 'include',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ credentialJson: JSON.stringify(credential) }),
});

Design notes

The options endpoints return the handler's JSON verbatim instead of round-tripping it through a DTO, so an app's JsonNamingPolicy cannot rename properties whose names WebAuthn fixes.

Registration compares the attested user entity ID against the signed-in user before storing, so a client cannot register a passkey onto another account by replaying someone else's attestation state.

Missing or expired ceremony state comes back as a 400. Taking too long between the two requests is a normal thing for a user to do, not a bug. A new internal PasskeyAuthenticationStateException marks that case; it derives from InvalidOperationException, so existing catch blocks are unaffected. Whether it should be public is the open question on the proposal.

A missing cookie scheme still throws, unlike expired state. It means the app was never configured for this, the same developer error as /login?useCookies=true on a bearer-only setup, and it already throws there.

The bearer and cookie selection in /login moved into a private helper that /passkeys/login shares. /login behaves exactly as before.

Not included

Listing, renaming and deleting passkeys (#68199)

The endpoints need the Identity.TwoFactorUserId cookie scheme to hold ceremony state between the two requests. AddIdentityApiEndpoints registers it, but an app wired for bearer tokens only cannot use passkeys.

Testing

MapIdentityApiTests drives all four endpoints through a deterministic IPasskeyHandler rather than a real authenticator, covering authorization, bearer and both cookie modes, user mismatch, invalid and expired state, unknown emails and custom JSON naming. SignInManagerTest covers persistent and non-persistent sign-in, the metrics tags and the two ceremony-state failures.

Resolves #67301

@rolandVi rolandVi added the area-identity Includes: Identity and providers label Aug 4, 2026
@rolandVi
rolandVi requested a review from rokonec August 4, 2026 15:51
@rolandVi

rolandVi commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
4 pipeline(s) were filtered out due to trigger conditions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-identity Includes: Identity and providers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant