Skip to content

feat(system-config): add magic_link_redirect_uris - #58

Merged
Bccorb merged 1 commit into
mainfrom
feat/magic-link-redirect-allowlist
Aug 31, 2026
Merged

feat(system-config): add magic_link_redirect_uris#58
Bccorb merged 1 commit into
mainfrom
feat/magic-link-redirect-allowlist

Conversation

@Bccorb

@Bccorb Bccorb commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

First link in fells-code/seamless-auth-api#242. That issue is blocked on this key existing here, since SystemConfigSchema lives in this package.

What and why

An exact-match allowlist of destinations a magic link may be sent to, empty by default.

seamless-auth-api already validates a requested destination, but against origins, which is the WebAuthn allowed-origins list. That covers a target whose host is already an origin and cannot express the two cases the feature exists for:

  • a custom application scheme, myapp://auth/magic
  • a universal link on a host that should not also be a WebAuthn origin, for example https://links.example.com when passkeys are bound to app.example.com

Neither has an origin worth comparing, hence exact match. Empty by default, so a deployment that sets nothing keeps comparing against origins exactly as it does today and nothing changes for it.

The part worth reviewing: not a bare URL check

Entries use a new exported RedirectTargetSchema, which is deliberately stricter than z.url(). I checked what z.url() actually accepts before using it:

PASS  "myapp://auth/magic"
PASS  "com.example.app://cb"
PASS  "javascript:alert(1)"     <-- accepted
FAIL  "not a url"

A magic link destination is rendered as an href in an email. A javascript: or data: entry stored in config would therefore be a script-execution sink, reachable by anyone who can write system config through the admin API. A bare z.url() would have accepted it.

javascript:, data:, vbscript:, file:, blob: and about: are refused; everything else is allowed. Denied rather than allowlisted because the whole point of the key is to permit arbitrary application schemes, which an allowlist of known-good schemes cannot express.

SystemConfigPatchSchema takes the field too, so the guard covers the admin write path and not only what a server seeds at boot. There is a test for both surfaces.

Verification

226 tests passing across 16 files, 8 of them new: the default, an application scheme, a non-origin universal link, the three dangerous schemes, a non-URL, and the patch surface. npm run typecheck, npm run lint, npm run format:check and npm run build all clean.

The repo's schema-exports convention test caught that RedirectTargetSchema needed a matching RedirectTarget type alias, which it now has.

What follows

Once this releases, seamless-auth-api#242 consumes the new version and passes the key into resolveMagicLinkUrl, which already takes an allowlist argument and passes an empty one today purely because there was nothing to read.

seamless-auth-api validates a requested magic link destination against origins,
which covers a target whose host is already a WebAuthn origin. It cannot express
the two cases that need this key: a custom application scheme such as
myapp://auth, and a universal link on a host that should not also be a WebAuthn
origin. Exact match, since neither has an origin worth comparing.

Empty by default, so a deployment that sets nothing keeps comparing against
origins exactly as it does now.

Entries are validated with RedirectTargetSchema rather than z.url(), which is
stricter on purpose. z.url() accepts anything the URL parser does, including
javascript:alert(1) and data:text/html, and a magic link destination is rendered
as an href in an email. Stored in config that would be a script-execution sink
reachable through the admin system-config API. Denied rather than allowlisted
because the point of the key is to permit arbitrary application schemes, which an
allowlist of known-good schemes could not express.

The patch schema takes the field too, so the guard covers the admin write path
and not only what a server seeds at boot.
@Bccorb
Bccorb merged commit 40eb9c5 into main Aug 31, 2026
1 check passed
@Bccorb
Bccorb deleted the feat/magic-link-redirect-allowlist branch August 31, 2026 19:55
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