Skip to content

feat(magic-link): dedicated redirect allowlist for custom schemes and non-WebAuthn hosts #242

Description

@Bccorb

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:

  1. the key added there, with a version bump
  2. 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)
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3FixesenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions