Skip to content

fix(passkey): accept signed origin under a per-client RP-ID suffix#106

Merged
windischb merged 1 commit into
developfrom
fix/passkey-origin-subdomain-of-rpid
Jun 29, 2026
Merged

fix(passkey): accept signed origin under a per-client RP-ID suffix#106
windischb merged 1 commit into
developfrom
fix/passkey-origin-subdomain-of-rpid

Conversation

@windischb

Copy link
Copy Markdown
Contributor

The bug (amZettel prod blocker)

A per-client WebAuthn RP-ID is meant to be a registrable suffix of the app origin — RP-ID amzettel.at for a page on https://app.amzettel.at. modgud derived the accepted FIDO2 origin as https://{rpId}, so the browser's real origin (https://app.amzettel.at) was not in the allow-list and MakeNewCredential / MakeAssertion threw → 400 "Passkey enrollment failed." (the native login assertion failed identically). Only reproduces when RP-ID ≠ app host (dev has them equal, so it was invisible).

Root cause: RealmFido2.BuildConfigurationorigins.Add($"https://{host}") with host == rpIdOverride. The RP-ID hash check passes; only the origin collides.

The fix (spec-aligned, Approach A)

Accept the origin the authenticator actually signed (read from clientDataJSON) when it is the RP-ID host or a subdomain of it — exactly the set WebAuthn already scopes to that RP-ID — instead of only https://{rpId}. The RP-ID hash + signature remain the primary boundary; this only widens the origin allow-list to legitimate callers and rejects look-alikes (amzettel.at.evil.com, evilamzettel.at).

No new admin config. Any origin a browser can present for an RP-ID is already under it, so a server-to-server client with empty AllowedCorsOrigins (like amzettel-web) works too — which is why this is preferred over the per-client allow-list option in the report.

Changes:

  • RealmFido2.BuildConfiguration gains additionalOrigins (filtered via new IsOriginUnderRpId) + TryGetClientDataOrigin; RealmScopedFido2Factory.CreateAsync threads it.
  • Native enroll-finish + native login-assertion read the signed origin and pass it. Web cookie register/login pass nothing → bit-identical.
  • DX: the enroll-finish catch now LogWarning(ex, …) so an origin mismatch is diagnosable instead of a reasonless 400.

Tests

  • RealmFido2Tests (15) — suffix filter accept/reject incl. look-alikes, origin extraction, BuildConfiguration add/no-add.
  • End-to-end enroll from a subdomain origin (https://sub.a.localhost under RP-ID a.localhost) → 200.
  • Full CocoarPasskeyGrantFlowTests (24) + web cookie passkey login green; full solution builds 0 errors.

🤖 Generated with Claude Code

A per-client WebAuthn RP-ID is meant to be a registrable SUFFIX of the app
origin (RP-ID amzettel.at for a page on app.amzettel.at). modgud derived the
accepted FIDO2 origin as https://{rpId}, so the browser's real origin
(https://app.amzettel.at) was not in the allow-list and MakeNewCredential /
MakeAssertion threw -> 400 "Passkey enrollment failed." (and the analogous
native login assertion failed the same way). Blocker reported by amZettel; only
reproduces when RP-ID != app host.

Fix (spec-aligned): accept the origin the authenticator actually signed when it
is the RP-ID host or a subdomain of it -- exactly the set WebAuthn already
scopes to that RP-ID -- instead of only https://{rpId}. The RP-ID hash and
signature checks remain the primary boundary; this only widens the origin
allow-list to legitimate callers, and rejects look-alikes (amzettel.at.evil.com,
evilamzettel.at). No new admin config: any origin the browser can present for an
RP-ID is already under it, so a server-to-server client (empty AllowedCorsOrigins)
works too.

- RealmFido2.BuildConfiguration gains additionalOrigins (filtered via the new
  IsOriginUnderRpId) + TryGetClientDataOrigin; RealmScopedFido2Factory.CreateAsync
  threads it.
- Native enroll-finish + native login-assertion read the signed origin from
  clientDataJSON and pass it. Web cookie register/login pass nothing -> unchanged.
- DX: the enroll-finish catch now LogWarning(ex, ...) so an origin mismatch is
  diagnosable instead of a reasonless 400.

Tests: RealmFido2Tests (suffix filter accept/reject incl. look-alikes, origin
extraction, BuildConfiguration add/no-add) + an end-to-end enroll from a
subdomain origin. Full CocoarPasskeyGrantFlowTests (24) + web cookie passkey
login green; full solution builds 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@windischb windischb merged commit 2dd396f into develop Jun 29, 2026
8 checks passed
@windischb windischb deleted the fix/passkey-origin-subdomain-of-rpid branch June 29, 2026 20:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant