fix(passkey): accept signed origin under a per-client RP-ID suffix#106
Merged
Conversation
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>
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.
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.atfor a page onhttps://app.amzettel.at. modgud derived the accepted FIDO2 origin ashttps://{rpId}, so the browser's real origin (https://app.amzettel.at) was not in the allow-list andMakeNewCredential/MakeAssertionthrew → 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.BuildConfiguration—origins.Add($"https://{host}")withhost == 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(likeamzettel-web) works too — which is why this is preferred over the per-client allow-list option in the report.Changes:
RealmFido2.BuildConfigurationgainsadditionalOrigins(filtered via newIsOriginUnderRpId) +TryGetClientDataOrigin;RealmScopedFido2Factory.CreateAsyncthreads it.catchnowLogWarning(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.https://sub.a.localhostunder RP-IDa.localhost) → 200.CocoarPasskeyGrantFlowTests(24) + web cookie passkey login green; full solution builds 0 errors.🤖 Generated with Claude Code