Skip to content

Fix email auth confirmation to use token-hash verification#373

Open
gianpaj wants to merge 13 commits into
mainfrom
fix/auth-email-signup-PKCE
Open

Fix email auth confirmation to use token-hash verification#373
gianpaj wants to merge 13 commits into
mainfrom
fix/auth-email-signup-PKCE

Conversation

@gianpaj

@gianpaj gianpaj commented May 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add a dedicated /auth/confirm route that verifies Supabase email links with token_hash and verifyOtp() instead of PKCE code exchange.
  • Keep /auth/callback for OAuth, while sharing safe redirect handling and callback marker cookie logic across auth flows.
  • Update signup and password-reset redirects so Supabase email templates point users at final same-origin destinations.
  • Document the required Supabase dashboard email template changes and add coverage for the new confirmation flow.

Testing

  • pnpm --filter @sexyvoice/web test -- tests/auth-confirm.test.ts tests/auth-callback.test.ts
  • pnpm --filter @sexyvoice/web exec playwright test e2e/auth-email.spec.ts --project=chromium
  • pnpm type-check
  • pnpm fixall ran but still fails on existing repo-wide Biome issues unrelated to this change

Add a dedicated /auth/confirm route that verifies Supabase email auth links
with token_hash and verifyOtp, avoiding PKCE code-verifier failures when users
open email links from another browser, device, or email client webview.

Keep /auth/callback for OAuth code exchanges, share safe redirect handling
between auth routes, and update signup/reset-password redirects to pass final
same-origin destinations for Supabase email templates.

Document the required Supabase dashboard email template changes and add Vitest
and Playwright coverage for the confirmation route, email signup redirect
target, malformed confirmation links, unsafe redirects, and verifyOtp failures.
Copilot AI review requested due to automatic review settings May 12, 2026 10:44
@bolt-new-by-stackblitz

Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@vercel

vercel Bot commented May 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sexyvoice Ready Ready Preview, Comment Jun 19, 2026 7:49am
sexyvoice-docs Ready Ready Preview, Comment Jun 19, 2026 7:49am

Request Review

@claude

claude Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @gianpaj's task in 4m 18s —— View job


Claude PR Review

  • Read repo AGENTS.md and the 11 changed files (+ related: oauth-callback-marker.ts, constants.ts, tests/setup.ts, auth-callback.test.ts, handle_new_user migration)
  • Trace the email signup, OAuth callback, and password recovery flows end-to-end
  • Posted inline comments for specific issues

Summary

Solid refactor. The /auth/callback vs /auth/confirm split correctly fixes the original PKCE-verifier-not-found problem for email links opened in a different browser, and the new shared lib/supabase/auth-redirect.ts helper is a good extraction. Tests cover the new route's success and failure paths.

I found one regression, one deployment-coordination risk, and a handful of smaller items. Details on the relevant lines via inline comments — high-level summary below.

Findings

🔴 Regression — analytics + Stripe side effects dropped for email signup
Email signups previously fired posthog.capture({ event: 'sign-up', login_type: 'email' }) and createOrRetrieveCustomer(user.id, user.email) inside /auth/callback. After this PR they finish at /auth/confirm, which does neither. PostHog will appear to lose all email signups after deploy; Stripe customer creation is still safe because the credits page calls it lazily (apps/web/app/[lang]/(dashboard)/dashboard/credits/page.tsx:37), but anything else that assumes a Stripe customer exists right after signup should be re-checked. See inline comment on apps/web/app/auth/confirm/route.ts:66-92. Recommend extracting a shared post-verification helper used by both routes.

🟡 Deployment-ordering risk — Supabase email templates must be updated before this code lands
/auth/callback no longer handles token_hash, and /auth/confirm is new. The Supabase dashboard email templates have to be flipped in lockstep with this deploy (and reverted last on rollback). The doc adds the new template snippets but doesn't call out the ordering requirement — see inline on docs/devops.md. A small compat shim on /auth/callback that forwards ?token_hash=... to /auth/confirm would make this order-independent and is cheap insurance.

🟡 Sentry telemetry hygiene — flow: 'oauth-callback' tag is now misleading
The sv_oauth_callback_ok marker cookie is now also set by /auth/confirm, so the middleware's "OAuth callback completed but dashboard session was missing" event will fire (with the OAuth tag) for users coming from email confirm links. Mostly a triage / alerting concern. See inline on apps/web/lib/supabase/middleware.ts:89-105.

🟢 Smaller items (inline)

  • Recovery flow without redirect_to always falls back to /en/protected/update-password and the email query param is dropped (route.ts:41).
  • getSafeAuthRedirectPath returns //evil.com for inputs like https://sexyvoice.ai//evil.com; same-origin so not exploitable, but worth normalizing leading slashes (auth-redirect.ts:20-33).
  • EMAIL_OTP_TYPES set can drift from the EmailOtpType union — satisfies would lock it (route.ts:12-22).
  • Test coverage gap: no happy-path assertion for type=recovery (auth-confirm.test.ts:50-79).
  • Reminder to verify Supabase's "Redirect URLs" allowlist contains the per-locale dashboard paths (signup-form.tsx:54).

Things I checked and confirmed are fine

  • Same-origin enforcement in getSafeAuthRedirectPath correctly rejects javascript:, cross-origin, and protocol-relative inputs.
  • verifyOtp is invoked only after the type guard narrows type to EmailOtpType.
  • Successful-confirm response sets the marker cookie via createAuthRedirectResponse; failure paths intentionally do not (so middleware doesn't trip on a real bad token).
  • Password reset now preserves the email query param all the way through to /protected/update-password (improvement over old behavior).
  • proxy.ts correctly skips locale-redirect for /auth/confirm, and middleware.ts adds it to publicRoutes.
  • E2E spec uses storageState: { cookies: [], origins: [] } for unauthenticated runs — good.
    • Branch: fix/auth-email-signup-PKCE

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new /auth/confirm route to handle server-side email OTP verification using token_hash, which improves the reliability of email-based authentication flows across different environments. It centralizes redirect validation and locale detection into a shared utility, updates the signup and password reset actions, and includes new unit and E2E tests. Feedback suggests enhancing the redirect utility to preserve URL hash fragments, ensuring that deep links are maintained after the authentication process.

Comment thread apps/web/lib/supabase/auth-redirect.ts Outdated
@argos-ci

argos-ci Bot commented May 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) ✅ No changes detected - Jun 19, 2026, 10:30 AM

Comment thread apps/web/app/auth/confirm/route.ts
Comment thread apps/web/app/auth/confirm/route.ts Outdated
Comment thread docs/devops.md
Comment thread apps/web/lib/supabase/auth-redirect.ts
Comment thread apps/web/app/auth/confirm/route.ts Outdated
Comment thread apps/web/tests/auth-confirm.test.ts
Comment thread apps/web/app/[lang]/(auth)/signup/signup-form.tsx Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the SSR auth flow so Supabase email confirmation/password recovery links are verified via token_hash + verifyOtp() on a dedicated /auth/confirm route (instead of relying on PKCE code exchange), while keeping /auth/callback focused on OAuth.

Changes:

  • Added /auth/confirm route that verifies token_hash email links and safely redirects to validated same-origin destinations.
  • Refactored safe redirect parsing + callback marker cookie setting into a shared auth-redirect helper and reused it in /auth/callback.
  • Updated signup/password-reset redirect targets, added Vitest + Playwright coverage, and documented required Supabase email template changes.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
docs/devops.md Documents new Supabase email template links using /auth/confirm with token_hash.
apps/web/tests/auth-confirm.test.ts Adds unit coverage for /auth/confirm success/failure/unsafe redirect handling.
apps/web/proxy.ts Ensures /auth/confirm bypasses proxy/session middleware handling like /auth/callback.
apps/web/lib/supabase/middleware.ts Marks /auth/confirm as a public route.
apps/web/lib/supabase/auth-redirect.ts Introduces shared safe-redirect + callback marker cookie redirect helper.
apps/web/e2e/E2E_TEST_PLAN.md Updates E2E inventory to include email auth coverage.
apps/web/e2e/auth-email.spec.ts Adds Playwright coverage ensuring signup uses final destinations (not /auth/callback) and malformed confirm links go to login.
apps/web/app/auth/confirm/route.ts Implements token-hash verification via verifyOtp() and safe same-origin redirect logic.
apps/web/app/auth/callback/route.ts Reuses shared redirect validation + marker cookie logic for OAuth callback flow.
apps/web/app/actions.ts Updates password reset redirect target to a final same-origin destination (not /auth/callback).
apps/web/app/[lang]/(auth)/signup/signup-form.tsx Updates emailRedirectTo to a final dashboard destination instead of /auth/callback.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/web/lib/supabase/auth-redirect.ts Outdated
Comment thread apps/web/lib/supabase/middleware.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 96af8b8ef3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/web/app/auth/confirm/route.ts
Copilot AI review requested due to automatic review settings May 28, 2026 18:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Resolve merge conflicts in signup form, auth callback route, and proxy:

- signup-form: keep main's server-side /auth/signup fetch with next-intl
- proxy: allow both /auth/confirm (email PKCE) and /auth/signup routes
- auth/callback: combine main's OAuth replay short-circuit and error
  handling with branch AUTH_* marker naming and auth-redirect helpers
- auth-callback tests: update imports to AUTH_* naming
* fix(auth): align email signup e2e with server-side signup route

Signup moved to POST /auth/signup, so Playwright mocks of the browser-side
Supabase auth/v1/signup endpoint no longer intercept anything.

- Pass locale from the signup form to the server route
- Set emailRedirectTo to the localized dashboard URL (not /auth/callback)
- Short-circuit Supabase in E2E mode and return emailRedirectTo in the response
- Update auth-email.spec.ts to assert the /auth/signup response payload

* fix(auth): stop trusting Origin header for email redirect URLs

Use NEXT_PUBLIC_SITE_URL as the primary origin for signup and password
reset redirects, with request.url as a fallback in route handlers only.
Reject signup when no trusted origin can be resolved.

Extract getSiteUrlOrigin/getRequestOrigin helpers into auth-redirect and
add unit tests covering spoofed Origin headers and invalid config.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a546dea297

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const user = data.user ?? (await supabase.auth.getUser()).data.user;

if (user) {
await recordSignupSideEffects(user, 'email');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard confirmation redirects from side-effect failures

If recordSignupSideEffects() rejects here (for example a Stripe search/create/update failure or PostHog shutdown error), the route has already successfully consumed the one-time token_hash via verifyOtp() but will throw a 500 instead of redirecting the newly confirmed user. The same email link then cannot be retried successfully, so these non-critical provisioning/analytics failures should be caught/logged while still returning the auth redirect.

Useful? React with 👍 / 👎.

Comment thread docs/devops.md
Comment on lines +248 to +252
Deployment order matters for these template changes:

1. Update the Supabase dashboard Confirm signup and Reset password templates.
2. Deploy the app version that serves `/auth/confirm` immediately after the
dashboard update.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Deploy the confirm route before switching templates

With this ordering, any signup or reset email generated after the dashboard template change but before the new app version is deployed will point at /auth/confirm, which the old deployment does not serve, leaving users with broken one-time auth links. The safer rollout order is to deploy the app route first, then switch the Supabase templates, and reverse that order for rollback.

Useful? React with 👍 / 👎.

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.

3 participants