Skip to content

auth.resend() consistent confirmation flow#2401

Open
weilirs wants to merge 1 commit intosupabase:masterfrom
weilirs:auth.resend-consistent-confirmation-flow
Open

auth.resend() consistent confirmation flow#2401
weilirs wants to merge 1 commit intosupabase:masterfrom
weilirs:auth.resend-consistent-confirmation-flow

Conversation

@weilirs
Copy link

@weilirs weilirs commented Feb 28, 2026

What kind of change does this PR introduce?

Bug fix

What is the current behavior?

The /resend endpoint hardcodes models.ImplicitFlow for both signup and email_change verification types (#42527). This means resent confirmation emails always use the implicit flow — redirecting with tokens in the URL hash fragment (#access_token=...) — even when the original signUp() used PKCE.

This creates an inconsistency where:

  • Initial signup email: https://example.com/auth/confirm?code=xxx (PKCE, works with server routes)
  • Resent email: https://example.com/auth/confirm#access_token=xxx (implicit, requires client-side handling)

Server-side route handlers (e.g., Next.js route.ts) cannot read hash fragments, forcing developers to implement workarounds with client components and dual flow handling.

Closes #42527

What is the new behavior?

The /resend endpoint now accepts optional code_challenge and code_challenge_method parameters for signup and email_change types. When provided, the endpoint:

  1. Determines the flow type from code_challenge (PKCE if present, implicit if absent)
  2. Creates a FlowState record for PKCE flows (needed by /verify to issue an auth code)
  3. Passes the correct flow type to sendConfirmation / sendEmailChange

This produces confirmation emails with ?code=... query params instead of #access_token=... hash fragments, consistent with the initial signup flow.

When code_challenge is not provided, behavior is unchanged — implicit flow is used, maintaining full backward compatibility.

Changes:

  • internal/api/resend.go: Added CodeChallenge and CodeChallengeMethod fields to ResendConfirmationParams. Added PKCE param validation for email-based types. Replaced hardcoded ImplicitFlow with flow-aware logic for signup and email_change cases.
  • internal/api/resend_test.go: Added TestResendPKCEValidation (invalid PKCE params return 400) and TestResendPKCESuccess (signup and email change tokens get pkce_ prefix when PKCE params are provided).

Additional context

This is the server-side half of the fix. The JS SDK (auth-js) needs a corresponding update to send code_challenge / code_challenge_method in resend() calls when flowType === 'pkce', following the same pattern already used by signUp() and signInWithOtp(). See this PR

The implementation mirrors the existing PKCE pattern used across the codebase (signup.go, user.go, recover.go, magic_link.go): getFlowFromChallenge → conditional generateFlowState → pass flowType to the email sender.

@weilirs weilirs requested a review from a team as a code owner February 28, 2026 13:45
@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ab7c9f9 and 284a68a.

📒 Files selected for processing (2)
  • internal/api/resend.go
  • internal/api/resend_test.go

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Added PKCE (Proof Key for Code Exchange) support to resend confirmation flows for enhanced security in signup and email change operations.
    • System now generates PKCE-specific confirmation tokens when PKCE parameters are provided.
  • Bug Fixes

    • Implemented validation to enforce required PKCE parameters, preventing incomplete security configurations.

Walkthrough

The changes add PKCE (Proof Key for Code Exchange) support to the resend confirmation API. The ResendConfirmationParams struct is extended with two new fields: CodeChallenge and CodeChallengeMethod. Validation logic now enforces the presence of PKCE parameters for signup and email_change flow types. The resend flow determines the flow type from the provided CodeChallenge and conditionally generates flow state for PKCE-enabled requests. Test coverage is expanded with validation and success test cases for PKCE scenarios.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ResendHandler as Resend Handler
    participant Validator as PKCE Validator
    participant FlowState as Flow State Generator
    participant MailService as Mail Service

    Client->>ResendHandler: POST /resend<br/>(with code_challenge, code_challenge_method)
    ResendHandler->>Validator: validatePKCEParams(method, challenge)
    
    alt PKCE params invalid
        Validator-->>ResendHandler: Error
        ResendHandler-->>Client: HTTP 400
    else PKCE params valid
        Validator-->>ResendHandler: Valid
        ResendHandler->>ResendHandler: Determine flowType from CodeChallenge
        alt PKCE flow detected
            ResendHandler->>FlowState: Generate flow state
            FlowState-->>ResendHandler: Flow state data
        end
        ResendHandler->>MailService: sendConfirmation/sendEmailChange(flowType)
        MailService->>MailService: Create PKCE-prefixed token
        MailService-->>Client: Confirmation sent
    end
Loading

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

return a.sendConfirmation(r, tx, user, models.ImplicitFlow)
flowType := getFlowFromChallenge(params.CodeChallenge)
if isPKCEFlow(flowType) {
if _, terr := generateFlowState(tx, models.EmailSignup.String(), models.EmailSignup, params.CodeChallengeMethod, params.CodeChallenge, &user.ID); terr != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

🟠 Severity: HIGH

The /resend endpoint allows unauthenticated users to create a new PKCE FlowState for any user. Since verification retrieves the latest FlowState by user_id, an attacker can overwrite a victim's flow. By providing a controlled redirect_to URL, the attacker can intercept the auth_code and hijack the session.
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: The /resend endpoint should not allow unauthenticated users to create or modify FlowState records with PKCE parameters for arbitrary users. The most secure fix is to reject PKCE parameters entirely on the /resend endpoint for signup and email_change verification types. Remove the PKCE flow handling code (lines 130-135 for signup and lines 147-152 for email_change) that calls generateFlowState(). The /resend endpoint is meant for retrying delivery of verification emails, not for initiating new PKCE authentication flows. Since this endpoint is unauthenticated and allows lookups by email, allowing PKCE flow creation creates an inherent authorization vulnerability where attackers can overwrite legitimate users' FlowState records with attacker-controlled code_challenge values.

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