Skip to content

Commit 8332c6f

Browse files
fix(next-auth): not redirecting to /undefined when signIn method throws an error (#11010)
* Fix NextAuth redirecting to /undefined when signIn method throws an error @auth/core `Auth` function can return an internalResponse or a Standard Response. Internal response has a `redirect` attribute. The problem is that the logic inside `Auth` method when there is an error is returning a Standard web Response which doesn't have `redirect` attribute but we can get redirecting url by using `headers.get('Origin')`. I think this fix this issue: #11008 * Move action test to tests folder * Remove test-adapter and add reference to docs * Defend from undefined responseUrl * Fix linter * chore(test): simplify tests --------- Co-authored-by: Thang Vu <[email protected]>
1 parent f6dbcd2 commit 8332c6f

File tree

5 files changed

+137
-12
lines changed

5 files changed

+137
-12
lines changed

docs/pages/guides/pages/signin.mdx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ We can now build our own custom sign in page.
9999

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

103104
export default async function SignInPage() {
104105
return (
@@ -107,7 +108,22 @@ export default async function SignInPage() {
107108
<form
108109
action={async () => {
109110
"use server"
110-
await signIn(provider.id)
111+
try {
112+
await signIn(provider.id)
113+
} catch (error) {
114+
// Signin can fail for a number of reasons, such as the user
115+
// not existing, or the user not having the correct role.
116+
// In some cases, you may want to redirect to a custom error
117+
if (error instanceof AuthError) {
118+
return redirect(`${SIGNIN_ERROR_URL}?error=${error.type}`)
119+
}
120+
121+
// Otherwise if a redirects happens NextJS can handle it
122+
// so you can just re-thrown the error and let NextJS handle it.
123+
// Docs:
124+
// https://nextjs.org/docs/app/api-reference/functions/redirect#server-component
125+
throw error
126+
}
111127
}}
112128
>
113129
<button type="submit">

packages/next-auth/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"clean": "rm -rf *.js *.d.ts* lib providers",
6767
"dev": "pnpm providers && tsc -w",
6868
"test": "vitest run -c ../utils/vitest.config.ts",
69+
"test:watch": "vitest -c ../utils/vitest.config.ts",
6970
"providers": "node ../utils/scripts/providers"
7071
},
7172
"files": [

packages/next-auth/src/lib/actions.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ import { Auth, raw, skipCSRFCheck, createActionURL } from "@auth/core"
22
import { headers as nextHeaders, cookies } from "next/headers"
33
import { redirect } from "next/navigation"
44

5-
import type { AuthAction } from "@auth/core/types"
65
import type { NextAuthConfig } from "./index.js"
76
import type { NextAuthResult, Session } from "../index.js"
87
import type { ProviderType } from "@auth/core/providers"
9-
import type { headers } from "next/headers"
108

119
type SignInParams = Parameters<NextAuthResult["signIn"]>
1210
export async function signIn(
@@ -73,8 +71,16 @@ export async function signIn(
7371

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

76-
if (shouldRedirect) return redirect(res.redirect!)
77-
return res.redirect as any
74+
const responseUrl =
75+
res instanceof Response ? res.headers.get("Location") : res.redirect
76+
77+
// NOTE: if for some unexpected reason the responseUrl is not set,
78+
// we redirect to the original url
79+
const redirectUrl = responseUrl ?? url
80+
81+
if (shouldRedirect) return redirect(redirectUrl)
82+
83+
return redirectUrl as any
7884
}
7985

8086
type SignOutParams = Parameters<NextAuthResult["signOut"]>
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
2+
import NextAuth, { NextAuthConfig } from "../src"
3+
// TODO: Move the MemoryAdapter to utils package
4+
import { MemoryAdapter } from "../../core/test/memory-adapter"
5+
import Nodemailer from "@auth/core/providers/nodemailer"
6+
7+
let mockedHeaders = vi.hoisted(() => {
8+
return new globalThis.Headers()
9+
})
10+
11+
const mockRedirect = vi.hoisted(() => vi.fn())
12+
13+
vi.mock("next/navigation", async (importOriginal) => {
14+
const originalModule = await importOriginal()
15+
return {
16+
// @ts-expect-error - not typed
17+
...originalModule,
18+
redirect: mockRedirect,
19+
}
20+
})
21+
22+
vi.mock("next/headers", async (importOriginal) => {
23+
const originalModule = await importOriginal()
24+
return {
25+
// @ts-expect-error - not typed
26+
...originalModule,
27+
headers: () => mockedHeaders,
28+
cookies: () => {
29+
const cookies: { [key: string]: unknown } = {}
30+
return {
31+
get: (name: string) => {
32+
return cookies[name]
33+
},
34+
set: (name: string, value: string) => {
35+
cookies[name] = value
36+
},
37+
}
38+
},
39+
}
40+
})
41+
42+
const options = {
43+
44+
} satisfies Parameters<ReturnType<typeof NextAuth>["signIn"]>[1]
45+
46+
let nextAuth: ReturnType<typeof NextAuth> | null = null
47+
48+
let config: NextAuthConfig = {
49+
adapter: MemoryAdapter(),
50+
providers: [
51+
Nodemailer({
52+
sendVerificationRequest() {
53+
// ignore
54+
},
55+
server: {
56+
host: "smtp.example.com",
57+
port: 465,
58+
secure: true,
59+
},
60+
}),
61+
],
62+
}
63+
64+
describe("signIn action", () => {
65+
beforeEach(() => {
66+
process.env.AUTH_SECRET = "secret"
67+
process.env.AUTH_URL = "http://localhost"
68+
nextAuth = NextAuth(config)
69+
})
70+
afterEach(() => {
71+
process.env.AUTH_SECRET = ""
72+
process.env.AUTH_URL = ""
73+
nextAuth = null
74+
vi.resetAllMocks()
75+
})
76+
describe("with Nodemailer provider", () => {
77+
it("redirects to /verify-request", async () => {
78+
await nextAuth?.signIn("nodemailer", options)
79+
expect(mockRedirect).toHaveBeenCalledWith(
80+
"http://localhost/api/auth/verify-request?provider=nodemailer&type=email"
81+
)
82+
})
83+
84+
it("redirects to /error page when sendVerificationRequest throws", async () => {
85+
nextAuth = NextAuth({
86+
...config,
87+
providers: [
88+
Nodemailer({
89+
sendVerificationRequest() {
90+
throw new Error()
91+
},
92+
server: {
93+
host: "smtp.example.com",
94+
port: 465,
95+
secure: true,
96+
},
97+
}),
98+
],
99+
})
100+
const redirectTo = await nextAuth.signIn("nodemailer", {
101+
...options,
102+
redirect: false,
103+
})
104+
expect(redirectTo).toEqual(
105+
"http://localhost/api/auth/error?error=Configuration"
106+
)
107+
})
108+
})
109+
})

packages/next-auth/test/index.test.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)