Follow-up to #141, which is closed by #240.
#240 added redirectUri to GET /magic-link, validated against the configured origins. That covers a tenant whose mobile destination is a path or host already among its WebAuthn origins, which is the common case, and deliberately stops short of two it does not:
- a custom scheme,
myapp://auth/magic, which is not a URL any WebAuthn origin can express
- a universal link on a host that should not be a WebAuthn origin, for example
https://links.example.com when passkeys are bound to app.example.com
Reusing origins also conflates two ideas. It is the WebAuthn allowed-origins list, and it now doubles as "where a magic link may land". A tenant that wants the second without the first has to widen the first, which is the wrong direction for a list that gates passkey ceremonies.
Scope
Add a magic_link_redirect_uris system config key, an array of exact-match targets, defaulting to [].
The matching already supports this. allowedRedirect in src/lib/redirectAllowlist.ts matches an explicit allowlist exactly and falls back to origin comparison only when the list is empty, which is how OAuth per-provider redirectUris already works. resolveMagicLinkUrl passes [] today purely because there is no key to read:
if (!allowedRedirect(requestedRedirectUri, [], config.origins)) {
So the change is the config key plus passing it in. Exact match is what makes a custom scheme safe: myapp://auth/magic has no origin to compare, and only an operator-listed literal should be accepted.
Why this is not already done
SystemConfigSchema lives in @seamless-auth/types, so this needs:
- the key added there, with a version bump
- this API to consume the new version, plus
SYSTEM_CONFIG_DEFAULTS and SYSTEM_CONFIG_ENV_MAP entries and a parseSystemConfigEnvValue case (array of strings, like origins)
- a coordinated release across this API and both SDKs
That is the cross-repo work CLAUDE.md calls out, which is why #240 stopped at the origins-based allowlist rather than bundling it.
Notes
No migration needed. bootstrapSystemConfig creates a row for any key missing from system_config using SYSTEM_CONFIG_DEFAULTS, so an instance that predates the key picks up [] on next boot and behaves exactly as it does now.
Documented as a known limit in docs/api-contract.md under Magic link destination; update that when this lands.
Follow-up to #141, which is closed by #240.
#240 added
redirectUritoGET /magic-link, validated against the configuredorigins. That covers a tenant whose mobile destination is a path or host already among its WebAuthn origins, which is the common case, and deliberately stops short of two it does not:myapp://auth/magic, which is not a URL any WebAuthn origin can expresshttps://links.example.comwhen passkeys are bound toapp.example.comReusing
originsalso conflates two ideas. It is the WebAuthn allowed-origins list, and it now doubles as "where a magic link may land". A tenant that wants the second without the first has to widen the first, which is the wrong direction for a list that gates passkey ceremonies.Scope
Add a
magic_link_redirect_urissystem config key, an array of exact-match targets, defaulting to[].The matching already supports this.
allowedRedirectin src/lib/redirectAllowlist.ts matches an explicit allowlist exactly and falls back to origin comparison only when the list is empty, which is how OAuth per-providerredirectUrisalready works.resolveMagicLinkUrlpasses[]today purely because there is no key to read:So the change is the config key plus passing it in. Exact match is what makes a custom scheme safe:
myapp://auth/magichas no origin to compare, and only an operator-listed literal should be accepted.Why this is not already done
SystemConfigSchemalives in@seamless-auth/types, so this needs:SYSTEM_CONFIG_DEFAULTSandSYSTEM_CONFIG_ENV_MAPentries and aparseSystemConfigEnvValuecase (array of strings, likeorigins)That is the cross-repo work CLAUDE.md calls out, which is why #240 stopped at the origins-based allowlist rather than bundling it.
Notes
No migration needed.
bootstrapSystemConfigcreates a row for any key missing fromsystem_configusingSYSTEM_CONFIG_DEFAULTS, so an instance that predates the key picks up[]on next boot and behaves exactly as it does now.Documented as a known limit in docs/api-contract.md under Magic link destination; update that when this lands.