feat(native-passkey): Bearer-authenticated passkey list + delete#105
Merged
Conversation
Add the cookieless, Bearer-authenticated half of native passkey management
so a native client (or a brokering web BFF) can show "your passkeys" and
revoke a lost device from the access token it already holds — closing the
gap left by the enroll/login ceremony, where the only list/delete surface
was the cookie-based realm UI (which needs a Modgud session).
- GET /connect/passkey — the token subject's own passkeys, user-scoped
(mirrors the cookie-based Passkey_List so both management surfaces agree).
- DELETE /connect/passkey/{id} — revoke one own passkey; an unknown id or
one owned by another user is a 404 (never 403 — no cross-user oracle).
Both gated by the per-realm NativeGrants flag and the OpenIddict validation
(Bearer) scheme, exactly like the enroll endpoints. The NativeGrants gate
and the store-backed (SecurityStamp-authoritative) principal resolution are
lifted into a shared NativeBearerEndpointSupport so the enroll and management
paths cannot drift.
Tests: 8 new integration tests (list owner-scoping + 401 + flag-off; delete
own/foreign/unknown/anon; and a delete-then-native-login-fails end-to-end
proving a revoked passkey is no longer redeemable). Full Cocoar native-grant
suite green (35). Docs: native-apps.md "Manage passkeys" section + error rows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Adds the cookieless, Bearer-authenticated half of native passkey management so a native client (iOS) or a brokering web BFF can offer a "your passkeys" / "revoke a lost device" surface from the access token it already holds.
Until now modgud could enroll a passkey and log in with it cookielessly (ADR-0009 / ADR-0010), but the only way to list or delete a user's passkeys was the cookie-based realm UI (
/api/account/passkey), which needs a modgud session — something neither the native client nor the amZettel BFF has. Driver FR: amZettel.The two endpoints
GET /connect/passkey[{ Id, DisplayName, CreatedAt, LastUsedAt }]DELETE /connect/passkey/{id}204; unknown id or one owned by another user →404(never403)NativeGrantsflag + the OpenIddict validation (Bearer) scheme.404-not-403keeps it from being a cross-user credential-existence oracle.urn:cocoar:passkeyassertion.Design note
The list is user-scoped (every passkey the user holds, not filtered by the calling client's RP-ID). Rationale: the FR's primary driver is "revoke a lost device" (needs full visibility), and it mirrors the existing cookie-based
Passkey_Listexactly, so both management surfaces agree. The FR flagged RP-ID filtering as "optional" — easy to add if we want a per-app view later.Refactor
The
NativeGrantsgate and the store-backed principal resolution are lifted out ofNativePasskeyEnrollEndpointsinto a sharedNativeBearerEndpointSupport, so the enroll and management paths can't drift on the security contract.Tests
8 new integration tests in
CocoarPasskeyGrantFlowTests.Management.cs:401anon,400whenNativeGrantsoff404+ untouched, unknown id →404,401anonFull
Cocoar*native-grant suite green (35); full solution (Modgud.slnx) builds with 0 errors.Docs
docs/integrate/native-apps.md— new "Manage passkeys — list & revoke" section + error-table rows.🤖 Generated with Claude Code