feat(connect): opt-in callback return for Connect & sign - #3
Merged
Conversation
Lets a requesting site receive the signed result automatically instead of the user copy/pasting the address and signature back. Opt-in and fully backward-compatible: a request without `callback` behaves exactly as today. A request envelope may now carry a `callback` URL. After a successful signature the result rides back in that URL's *fragment* (`#nonce=..&address=..&signature=..`, each value encodeURIComponent'd). The fragment is never sent to a server, so the signature stays out of access logs, proxy logs, and the Referer header — and the requesting page reads it client-side. A query string would leak it; a cross-origin POST form is blocked by our own `form-action 'self'` CSP anyway. Origin-binding is the check that matters and is enforced at parse time, so a callback that fails it never reaches the UI: the field is simply absent and the user falls back to the manual return. A callback is kept only when the envelope declares an `origin` and the callback resolves to that exact origin (scheme, host, and port). This stops site A routing site B's signed challenge to an attacker-controlled callback. Also refused: non-http(s) schemes, relative URLs, and embedded credentials; any pre-existing fragment is stripped since the fragment is ours. Auto-return is gated on the request having arrived via the `?req=` deep link, and never fires in the Capacitor shell. Photonic ships web, Tauri and native from one bundle, where navigating the window to a remote page would replace the wallet app itself with no way back. A pasted or scanned request returns by copy/paste, which is what the other tab or device awaits anyway. Tests cover the contract's test vector byte-for-byte, the binding rejections (cross-origin, host-suffix, scheme/port), fragment-not-query, and nonce omission for freeform challenges.
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.
Implements the Photonic sign-in callback contract, letting a requesting site receive the signed result automatically instead of the user copy/pasting the address and signature back.
Opt-in and fully backward-compatible. A request without
callbackbehaves exactly as it does today — the manual copy/paste and QR return are untouched. SURF.RXD already implements its half of this.What changed
A request envelope may now carry an optional
callbackURL. After a successful signature, the result rides back in that URL's fragment:Each value is
encodeURIComponent-escaped, since a base64 signature contains+,/and=. The fragment is never sent to a server, so the signature stays out of access logs, proxy logs, and theRefererheader, and the requesting page reads it client-side. A query string would leak it into logs and history; a cross-origin POST form is blocked by our ownform-action 'self'CSP anyway.packages/app/src/connect/protocol.ts— thecallbackfield,extractChallengeNonce,buildCallbackUrl, and the origin-binding validation.packages/app/src/pages/Connect.tsx— the redirect after signing, plus pre-approval disclosure telling the user they'll be sent back to the named origin, which receives their address and signature.Security
Origin-binding is enforced at parse time, so a callback that fails it never reaches the UI — the field is simply absent and the user falls back to the manual return. A callback is kept only when the envelope declares an
originand the callback resolves to that exact origin (scheme, host, and port). This is what stops site A routing site B's signed challenge to an attacker-controlled callback. Also refused: non-http(s) schemes such asjavascript:, relative URLs, and embedded credentials. Any pre-existing fragment is stripped, since the fragment is ours.Exposure is bounded regardless: the signature is over a single-use, server-issued nonce, which SURF marks consumed on first verify — so a leaked signature can't be replayed against a different challenge.
Judgement call worth a look
Auto-return is gated on the request having arrived via the
?req=deep link, and never fires in the Capacitor shell (canAutoReturn). Photonic ships web, Tauri, and native from one bundle, and in those shells navigating the window to a remote page replaces the wallet app itself with no way back. A pasted or scanned request returns the classic way, which is what the user's other tab or device is waiting for anyway. Happy to drop the gate if you'd rather redirect on any request carrying a valid callback — it's a one-line change.Testing
src/connect, covering the contract's test vector byte-for-byte, the binding rejections (cross-origin, host-suffix likesurf.rxd.zone.evil.example, scheme/port downgrades), fragment-not-query, and nonce omission for freeform challenges.