nextauthjs / nextauthjs/next-auth

Unable to Trigger Custom Sign In Flow after User Creation via NextAuth V4

Open
#9,397 5 comments 4 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug providers triage
Dominant language
TypeScript
Stars
28.4k
Forks
4k
PR merge metrics
No merged PRs in 30d

Description

Provider type

Email

Environment
System:
    OS: macOS 14.1.2
    CPU: (12) arm64 Apple M2 Max
    Memory: 2.07 GB / 32.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 20.8.0 - ~/.nvm/versions/node/v20.8.0/bin/node
    npm: 10.1.0 - ~/.nvm/versions/node/v20.8.0/bin/npm
    pnpm: 8.6.12 - /opt/homebrew/bin/pnpm
    bun: 1.0.7 - ~/.bun/bin/bun
  Browsers:
    Chrome: 120.0.6099.109
    Chrome Canary: 122.0.6188.0
    Edge: 120.0.2210.77
    Safari: 17.1.2
  npmPackages:
    @auth/drizzle-adapter: ^0.3.6 => 0.3.6 
    next: 14.0.3 => 14.0.3 
    next-auth: ^4.24.5 => 4.24.5 
    react: 18.2.0 => 18.2.0 
Reproduction URL

https://github.com/juancamiloqhz/next-auth-signup-issue

Describe the issue

Hello everyone,

I am currently developing an application utilizing create-t3-app along with NextAuth and Drizzle. I am employing a database session strategy and an email provider for passwordless authentication.

However, I have encountered a unique issue when attempting to implement a custom sign-up flow. In this process, users are required to sign up with their name, username, and email, which is managed via a custom API route at /api/signup.

This sequence proceeds as expected, where I validate the email and username for uniqueness in my database before account creation. The problem arises after the user is registered, wherein I attempt to spawn the NextAuth endpoint at /api/auth/signin/email. Unfortunately, I am unable to trigger the Sign In flow, and as a consequence, the user is not sent an email with the login link.

Here you can find the flow initiation within my react component:

async function onSubmit(data: FormData) {
    setIsLoading(true)
    setFormErrorMessage(null)
    setFormSuccessMessage({ title: null, message: null })

    try {
      const signInResult = await fetch("/api/signup", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          email: data.email.toLowerCase(),
          name: data.name,
          role: "user",
          username: data.username,
          callbackUrl: searchParams?.get("from") ?? "/dashboard",
        }),
      })

      const json = (await signInResult.json()) as {
        ok: boolean
        error: string | null
        url: string
        status: number
      }

      if (!json.ok && json.error) {
        return setFormErrorMessage(json.error)
      }

      setFormSuccessMessage({
        title: "¡Account created!",
        message:
          "We sent you an email with the login link. Please check the spam folder.",
      })
    } catch (error: unknown) {
      console.error("error: ", error)
      if (error instanceof Error) {
        setFormErrorMessage(
          "An error has occurred, please try again later."
        )
      }
    } finally {
      setIsLoading(false)
    }
  }

And here is the custom signup endpoint at /api/signup:

import { NextResponse } from "next/server"
import { db } from "@/server/db"
import { UserRole, users } from "@/server/db/schema"
import { getCsrfToken } from "next-auth/react"
import { z } from "zod"

import { env } from "@/env"

type SignUpRole = Exclude<UserRole, UserRole.Admin>

const signupSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  username: z
    .string()
    .refine(
      (username) =>
        /^[^\s]*$/.test(username) && /^[a-zA-Z0-9_-]+$/.test(username),
      {
        message:
          "Username cannot contain spaces or special characters (except underscore, hyphen)",
      }
    ),
  role: z.nativeEnum(UserRole).refine((role) => role !== UserRole.Admin, {
    message: "Role should not be admin",
  }),
  callbackUrl: z.string(),
})

export async function POST(req: Request) {
  try {
    const { name, email, username, role, callbackUrl } = (await req.json()) as {
      name: string
      email: string
      username: string
      role: SignUpRole
      callbackUrl: string
    }

    // Validate data
    const result = await signupSchema.safeParseAsync({
      name,
      email,
      username,
      role,
      callbackUrl,
    })

    if (!result.success) {
      return new Response("Invalid data.", { status: 400 })
    }

    // Check if email is already in use
    const existingUser = await db.query.users.findFirst({
      where: (queryUser, { eq }) => eq(queryUser.email, email.toLowerCase()),
    })

    if (existingUser) {
      return NextResponse.json(
        {
          error: "This email is already in use.",
          status: 409,
          ok: false,
          url: null,
        },
        { status: 409 }
      )
    }

    // Check if username is already in use
    const existingUsername = await db.query.users.findFirst({
      where: (queryUser, { eq }) => eq(queryUser.username, username),
    })

    if (existingUsername) {
      return NextResponse.json(
        {
          error: "This username is already in use",
          status: 409,
          ok: false,
          url: null,
        },
        { status: 409 }
      )
    }

    // Create user
    await db.insert(users).values({
      id: crypto.randomUUID(),
      name,
      email: email.toLowerCase(),
      username: username.toLowerCase(),
      role,
    })

    // Call signin API route from next-auth
    const res = await fetch(
      `${env.NEXT_PUBLIC_APP_URL}/api/auth/signin/email`,
      {
        method: "post",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        // @ts-expect-error - Is how they have it in next-auth signin method
        body: new URLSearchParams({
          email,
          csrfToken: await getCsrfToken(),
          redirect: false,
          callbackUrl,
          json: true,
        }),
      }
    )

    console.log("res: ", res.ok, res.status, res.statusText)

    const data = (await res.json()) as { url: string }

    const error = new URL(data.url).searchParams.get("error")

    return NextResponse.json(
      {
        error,
        status: res.status,
        ok: res.ok,
        url: error ? null : data.url,
      },
      { status: 201 }
    )
  } catch (error) {
    console.log("Error: ", error)
    return new Response("Error", { status: 500 })
  }
}

When I try to perform a sign-up operation, the response from /api/signup endpoint is:

{
    "error": null,
    "status": 200,
    "ok": true,
    "url": "http://localhost:3000/api/auth/signin?csrf=true"
}

This implies that the NextAuth sign-in flow is not being initiated, and the user does not receive the intended sign-in email. Any help in addressing this situation would be greatly appreciated.

How to reproduce

Prerequisites:

Make sure you have the following prerequisites installed:

  • Docker Desktop
  • Node (v20.8.0)
  • pnpm

Steps to Reproduce the Issue:

Follow the step-by-step instructions below to reproduce the issue in the local environment:

  1. Begin by cloning the repository to your local machine. https://github.com/juancamiloqhz/next-auth-signup-issue

  2. Once that is done, run the command make dev. This command will install necessary dependencies and initiate a new MySQL database in Docker.

  3. (Optional Step) Run make db-seed if you want to seed the database.

  4. Start the development server by running the make dev command.

  5. Next, navigate to http://localhost:3000/register in your browser.

  6. Attempt to sign up as either a user or a company:

    • If signing up as a user, refer to the code under src/components/signup-user-form.tsx.
    • If signing up as a company, refer to the code under src/components/signup-company-form.tsx.

Both options call the `/api/signup' endpoint which contains the behaviour I described above.

Expected behavior

Expected Behavior:

Upon a successful call to the /api/signup endpoint, we expect the sign-in process to be initiated following the call to /api/auth/signin/email.

According to the configuration in auth.ts (NextAuth options), within the sendVerificationRequest function, a login link should be logged out on the server console.

Current Behavior:

Contrary to expectations, upon calling the /api/signup endpoint, the sign-in flow doesn't proceed as anticipated. Consequently, no login URL is displayed on the server console. As a result, the authentication process isn't completed, preventing users who newly registered from receiving the sign-in email.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the /api/signup endpoint and the sign-in calls from src/components/signup-user-form.tsx and src/components/signup-company-form.tsx. Compare its request to /api/auth/signin/email and inspect auth.ts, especially sendVerificationRequest, using the linked reproduction repository. Done means a successful signup initiates the email sign-in flow and produces the expected login URL.

Written by the indexing model from the issue text.

Assessment

Tech stack
next.js, react, typescript
Domain
api, authentication, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.