Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(next-auth): redirecting to /undefined when signIn method throws an error #11010

18 changes: 17 additions & 1 deletion docs/pages/guides/pages/signin.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ We can now build our own custom sign in page.

```tsx filename="app/signin/page.tsx" /providerMap/
import { signIn, auth, providerMap } from "@/auth.ts"
import { AuthError } from "next-auth"

export default async function SignInPage() {
return (
Expand All @@ -107,7 +108,22 @@ export default async function SignInPage() {
<form
action={async () => {
"use server"
await signIn(provider.id)
try {
await signIn(provider.id)
} catch (error) {
// Signin can fail for a number of reasons, such as the user
// not existing, or the user not having the correct role.
// In some cases, you may want to redirect to a custom error
if (error instanceof AuthError) {
return redirect(`${SIGNIN_ERROR_URL}?error=${error.type}`)
}

// Otherwise if a redirects happens NextJS can handle it
// so you can just re-thrown the error and let NextJS handle it.
// Docs:
// https://nextjs.org/docs/app/api-reference/functions/redirect#server-component
throw error
}
andresgutgon marked this conversation as resolved.
Show resolved Hide resolved
}}
>
<button type="submit">
Expand Down
1 change: 1 addition & 0 deletions packages/next-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"clean": "rm -rf *.js *.d.ts* lib providers",
"dev": "pnpm providers && tsc -w",
"test": "vitest run -c ../utils/vitest.config.ts",
"test:watch": "vitest -c ../utils/vitest.config.ts",
"providers": "node ../utils/scripts/providers"
},
"files": [
Expand Down
14 changes: 10 additions & 4 deletions packages/next-auth/src/lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ import { Auth, raw, skipCSRFCheck, createActionURL } from "@auth/core"
import { headers as nextHeaders, cookies } from "next/headers"
import { redirect } from "next/navigation"

import type { AuthAction } from "@auth/core/types"
import type { NextAuthConfig } from "./index.js"
import type { NextAuthResult, Session } from "../index.js"
import type { ProviderType } from "@auth/core/providers"
import type { headers } from "next/headers"
ThangHuuVu marked this conversation as resolved.
Show resolved Hide resolved

type SignInParams = Parameters<NextAuthResult["signIn"]>
export async function signIn(
Expand Down Expand Up @@ -73,8 +71,16 @@ export async function signIn(

for (const c of res?.cookies ?? []) cookies().set(c.name, c.value, c.options)

if (shouldRedirect) return redirect(res.redirect!)
return res.redirect as any
const responseUrl =
res instanceof Response ? res.headers.get("Location") : res.redirect

// NOTE: if for some unexpected reason the responseUrl is not set,
// we redirect to the original url
const redirectUrl = responseUrl ?? url

if (shouldRedirect) return redirect(redirectUrl)

return redirectUrl as any
}

type SignOutParams = Parameters<NextAuthResult["signOut"]>
Expand Down
109 changes: 109 additions & 0 deletions packages/next-auth/test/actions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import NextAuth, { NextAuthConfig } from "../src"
// TODO: Move the MemoryAdapter to utils package
import { MemoryAdapter } from "../../core/test/memory-adapter"
import Nodemailer from "@auth/core/providers/nodemailer"

let mockedHeaders = vi.hoisted(() => {
return new globalThis.Headers()
})

const mockRedirect = vi.hoisted(() => vi.fn())

vi.mock("next/navigation", async (importOriginal) => {
const originalModule = await importOriginal()
return {
// @ts-expect-error - not typed
...originalModule,
redirect: mockRedirect,
}
})

vi.mock("next/headers", async (importOriginal) => {
const originalModule = await importOriginal()
return {
// @ts-expect-error - not typed
...originalModule,
headers: () => mockedHeaders,
cookies: () => {
const cookies: { [key: string]: unknown } = {}
return {
get: (name: string) => {
return cookies[name]
},
set: (name: string, value: string) => {
cookies[name] = value
},
}
},
}
})

const options = {
email: "jane@example.com",
} satisfies Parameters<ReturnType<typeof NextAuth>["signIn"]>[1]

let nextAuth: ReturnType<typeof NextAuth> | null = null

let config: NextAuthConfig = {
adapter: MemoryAdapter(),
providers: [
Nodemailer({
sendVerificationRequest() {
// ignore
},
server: {
host: "smtp.example.com",
port: 465,
secure: true,
},
}),
],
}

describe("signIn action", () => {
beforeEach(() => {
process.env.AUTH_SECRET = "secret"
process.env.AUTH_URL = "http://localhost"
nextAuth = NextAuth(config)
})
afterEach(() => {
process.env.AUTH_SECRET = ""
process.env.AUTH_URL = ""
nextAuth = null
vi.resetAllMocks()
})
describe("with Nodemailer provider", () => {
it("redirects to /verify-request", async () => {
await nextAuth?.signIn("nodemailer", options)
expect(mockRedirect).toHaveBeenCalledWith(
"http://localhost/api/auth/verify-request?provider=nodemailer&type=email"
)
})

it("redirects to /error page when sendVerificationRequest throws", async () => {
nextAuth = NextAuth({
...config,
providers: [
Nodemailer({
sendVerificationRequest() {
throw new Error()
},
server: {
host: "smtp.example.com",
port: 465,
secure: true,
},
}),
],
})
const redirectTo = await nextAuth.signIn("nodemailer", {
...options,
redirect: false,
})
expect(redirectTo).toEqual(
"http://localhost/api/auth/error?error=Configuration"
)
})
})
})
7 changes: 0 additions & 7 deletions packages/next-auth/test/index.test.ts

This file was deleted.

Loading