Skip to content

.ConfirmationURL drops path/query from redirect_to at email-send time, even though /auth/v1/verify honors it correctly when supplied directly #2722

Description

@mmzbht

Environment:

  • Project ref: scilqibpvkaopnsxyaqs
  • Site URL: https://crew.ridif.app
  • Redirect URLs allow-list: both https://crew.ridif.app/** (wildcard) and https://crew.ridif.app/complete-profile (exact literal) tested, same result either way

Summary:
When requesting a sign-in email via signInWithOtp/POST /auth/v1/otp with options.emailRedirectTo set to a URL containing a path or query string, the resulting email's {{ .ConfirmationURL }} always resolves to the bare Site URL — the path/query is silently dropped — even though the exact same value is present on the Redirect URLs allow-list and is honored correctly when applied directly to /auth/v1/verify.

Reproduction:

  1. POST https://scilqibpvkaopnsxyaqs.supabase.co/auth/v1/otp
    {"email":"<test address>","create_user":true,"options":{"email_redirect_to":"https://crew.ridif.app/complete-profile"}}
  2. Inspect the sent email's link (via a service that exposes raw email source, e.g. Mailinator's LINKS view — not just the rendered button, which can visually mask the actual href). It reads:
    https://scilqibpvkaopnsxyaqs.supabase.co/auth/v1/verify?token=<hash>&type=signup&redirect_to=https://crew.ridif.app
    
    — the requested /complete-profile path is gone.
  3. Repeat for a returning user (confirm the account first, then request sign-in again for the same address) to get a genuine type=magiclink email rather than type=signup. Same result: redirect_to=https://crew.ridif.app, path stripped. Confirmed via the actual email's type=magiclink value and a distinct subject line, not assumed.
  4. Decisive isolation step: take the token from step 2 or 3 and manually issue
    GET /auth/v1/verify?token=<same token>&type=<signup|magiclink>&redirect_to=https://crew.ridif.app/complete-profile
    
    directly (bypassing the emailed link, supplying redirect_to on the request myself). This succeeds and correctly returns a 303 to https://crew.ridif.app/complete-profile#access_token=... — full path preserved.

Conclusion this points to: the Redirect URLs allow-list matching is correct, and /auth/v1/verify respects it when redirect_to is supplied directly on the request. The bug is isolated to send-time construction of .ConfirmationURL for the outgoing email — that code path isn't applying the same allow-list match (or is applying it incorrectly), and falls back to bare Site URL instead of erroring or matching.

Template used (ruling out a template authoring mistake): our Magic Link template already correctly references {{ .ConfirmationURL }}, not {{ .SiteURL }}:

<h2>Sign in to your Ridif Crew Account</h2>
<p>Click the link below to sign in, or enter this code on the sign-in page.</p>
<p>They work once and expire in 1 hour.</p>
<p><a href="{{ .ConfirmationURL }}">Sign in</a></p>
<p>Code: {{ .Token }}</p>

This rules out the common "template hardcodes .SiteURL" misconfiguration — the template variable itself is populated incorrectly server-side.

Expected: .ConfirmationURL's embedded redirect_to should preserve the full requested path/query whenever it matches an allow-list entry, consistent with how /auth/v1/verify already behaves when the same value is supplied directly.

Current workaround in use: bypassing .ConfirmationURL entirely and constructing the verify link manually in the template using {{ .TokenHash }} + {{ .RedirectTo }} against the known project Auth host, e.g.:

<a href="https://scilqibpvkaopnsxyaqs.supabase.co/auth/v1/verify?token_hash={{ .TokenHash }}&type=magiclink&redirect_to={{ .RedirectTo }}">Sign in</a>

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions