Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit cef0f19

Browse files
committedMay 26, 2024·
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
1 parent fc0e10a commit cef0f19

File tree

12 files changed

+350
-6
lines changed

12 files changed

+350
-6
lines changed
 

‎docs/pages/guides/pages/signin.mdx

Lines changed: 15 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,20 @@ 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+
throw error
124+
}
111125
}}
112126
>
113127
<button type="submit">

‎docs/pages/reference/_meta.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,5 @@ export default {
4141
"unstorage-adapter": "@auth/unstorage-adapter",
4242
"upstash-redis-adapter": "@auth/upstash-redis-adapter",
4343
"xata-adapter": "@auth/xata-adapter",
44+
"test-adapter": "@auth/test-adapter",
4445
}

‎packages/adapter-test/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<p align="center">
2+
<br/>
3+
<a href="https://authjs.dev" target="_blank">
4+
<img height="64px" src="https://authjs.dev/img/logo-sm.png" />
5+
</a>
6+
<h3 align="center"><b>Test Adapter</b> - NextAuth.js / Auth.js</a></h3>
7+
<p align="center" style="align: center;">
8+
<a href="https://npm.im/@auth/pg-adapter">
9+
<img src="https://img.shields.io/badge/TypeScript-blue?style=flat-square" alt="TypeScript" />
10+
</a>
11+
<a href="https://npm.im/@auth/test-adapter">
12+
<img alt="npm" src="https://img.shields.io/npm/v/@auth/pg-adapter?color=green&label=@auth/test-adapter&style=flat-square">
13+
</a>
14+
<a href="https://www.npmtrends.com/@auth/pg-adapter">
15+
<img src="https://img.shields.io/npm/dm/@auth/test-adapter?label=%20downloads&style=flat-square" alt="Downloads" />
16+
</a>
17+
<a href="https://github.com/nextauthjs/next-auth/stargazers">
18+
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth?style=flat-square" alt="GitHub Stars" />
19+
</a>
20+
</p>
21+
</p>
22+
23+
---
24+
25+
Check out the documentation at [authjs.dev](https://authjs.dev/reference/adapter/test).

‎packages/adapter-test/package.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "@auth/test-adapter",
3+
"version": "0.1.0",
4+
"description": "Testing adapter for next-auth.",
5+
"homepage": "https://authjs.dev",
6+
"repository": "https://github.com/nextauthjs/next-auth",
7+
"bugs": {
8+
"url": "https://github.com/nextauthjs/next-auth/issues"
9+
},
10+
"author": "Jake Coppinger",
11+
"contributors": [
12+
"Thang Huu Vu <hi@thvu.dev>"
13+
],
14+
"license": "ISC",
15+
"keywords": [
16+
"next-auth",
17+
"@auth",
18+
"Auth.js",
19+
"next.js",
20+
"oauth"
21+
],
22+
"type": "module",
23+
"exports": {
24+
".": {
25+
"types": "./index.d.ts",
26+
"import": "./index.js"
27+
}
28+
},
29+
"files": [
30+
"*.d.ts*",
31+
"*.js",
32+
"src"
33+
],
34+
"private": false,
35+
"publishConfig": {
36+
"access": "public"
37+
},
38+
"scripts": {
39+
"build": "tsc"
40+
},
41+
"dependencies": {
42+
"@auth/core": "workspace:*"
43+
}
44+
}

‎packages/adapter-test/src/index.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import {
2+
Adapter,
3+
AdapterSession,
4+
AdapterUser,
5+
VerificationToken,
6+
AdapterAccount,
7+
} from "@auth/core/adapters"
8+
9+
export function TestAdapter(): Adapter {
10+
let user: AdapterUser | null = null
11+
let session: AdapterSession | null = null
12+
let account: AdapterAccount | null = null
13+
let verificationToken: VerificationToken | null = null
14+
return {
15+
async createUser(data: AdapterUser) {
16+
user = data
17+
return user
18+
},
19+
async getUser(userId: string) {
20+
return user && user.id === userId ? user : null
21+
},
22+
async getUserByEmail(email: string) {
23+
return user && user.email === email ? user : null
24+
},
25+
async createSession(data: {
26+
sessionToken: string
27+
userId: string
28+
expires: Date
29+
}) {
30+
session = {
31+
sessionToken: data.sessionToken,
32+
userId: data.userId,
33+
expires: data.expires,
34+
}
35+
return Promise.resolve(session)
36+
},
37+
async getSessionAndUser(sessionToken: string) {
38+
const result =
39+
session && session.sessionToken === sessionToken
40+
? { session: session!, user: user! }
41+
: null
42+
return Promise.resolve(result)
43+
},
44+
async updateUser(data: Partial<AdapterUser> & Pick<AdapterUser, "id">) {
45+
if (user?.id !== data.id) {
46+
throw new Error("No user id.")
47+
}
48+
49+
user = { ...user, ...data }
50+
return user
51+
},
52+
async updateSession(
53+
data: Partial<AdapterSession> & Pick<AdapterSession, "sessionToken">
54+
) {
55+
if (!session || session.sessionToken !== data.sessionToken) return null
56+
57+
session = { ...session, ...data }
58+
return session
59+
},
60+
async linkAccount(data: AdapterAccount) {
61+
account = data
62+
return Promise.resolve(account)
63+
},
64+
async getUserByAccount(
65+
input: Pick<AdapterAccount, "provider" | "providerAccountId">
66+
) {
67+
return account &&
68+
account.provider === input.provider &&
69+
account.providerAccountId === input.providerAccountId
70+
? user
71+
: null
72+
},
73+
async deleteSession(sessionToken: string) {
74+
if (!session || session.sessionToken !== sessionToken) return null
75+
session = null
76+
},
77+
async createVerificationToken(data: VerificationToken) {
78+
verificationToken = data
79+
return verificationToken
80+
},
81+
async useVerificationToken(params: { identifier: string; token: string }) {
82+
if (!verificationToken) return null
83+
if (
84+
verificationToken.identifier === params.identifier &&
85+
verificationToken.token === params.token
86+
) {
87+
verificationToken = null
88+
return verificationToken
89+
}
90+
91+
return null
92+
},
93+
async deleteUser(id: string) {
94+
if (user?.id !== id) return undefined
95+
user = null
96+
},
97+
async unlinkAccount(
98+
params: Pick<AdapterAccount, "provider" | "providerAccountId">
99+
) {
100+
if (
101+
!account ||
102+
account.provider !== params.provider ||
103+
account.providerAccountId !== params.providerAccountId
104+
) {
105+
return undefined
106+
}
107+
account = null
108+
},
109+
}
110+
}

‎packages/adapter-test/tsconfig.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "utils/tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": ".",
5+
"rootDir": "src"
6+
},
7+
"exclude": ["*.js", "*.d.ts"],
8+
"include": ["src/**/*"]
9+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// @ts-check
2+
3+
/**
4+
* @type {import('typedoc').TypeDocOptions & import('typedoc-plugin-markdown').MarkdownTheme}
5+
*/
6+
module.exports = {
7+
entryPoints: ["src/index.ts"],
8+
entryPointStrategy: "expand",
9+
tsconfig: "./tsconfig.json",
10+
entryModule: "@auth/test-adapter",
11+
entryFileName: "../test-adapter.mdx",
12+
includeVersion: true,
13+
readme: 'none',
14+
}

‎packages/core/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,9 @@ export async function Auth(
170170
return Response.json({ url }, { headers: response.headers })
171171
} catch (e) {
172172
const error = e as Error
173-
logger.error(error)
173+
if (process.env.NODE_ENV !== "test") {
174+
logger.error(error)
175+
}
174176

175177
const isAuthError = error instanceof AuthError
176178
if (isAuthError && isRaw && !isRedirect) throw error

‎packages/next-auth/package.json

Lines changed: 2 additions & 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": [
@@ -99,6 +100,7 @@
99100
},
100101
"devDependencies": {
101102
"@types/react": "18.0.37",
103+
"@auth/test-adapter": "workspace:*",
102104
"next": "14.0.3-canary.1",
103105
"nodemailer": "^6.9.3",
104106
"react": "^18.2.0"
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
2+
import { EmailProviderType } from "../providers"
3+
import { TestAdapter } from "@auth/test-adapter"
4+
import { signIn } from "./actions"
5+
import { NextAuthConfig } from "."
6+
7+
let mockedHeaders = vi.hoisted(() => {
8+
return new globalThis.Headers()
9+
})
10+
11+
const mockRedirect = vi.hoisted(() => vi.fn())
12+
vi.mock("next/navigation", async (importOriginal) => {
13+
const originalModule = await importOriginal()
14+
return {
15+
// @ts-expect-error - not typed
16+
...originalModule,
17+
redirect: mockRedirect,
18+
}
19+
})
20+
21+
vi.mock("next/headers", async (importOriginal) => {
22+
const originalModule = await importOriginal()
23+
return {
24+
// @ts-expect-error - not typed
25+
...originalModule,
26+
headers: () => mockedHeaders,
27+
cookies: () => {
28+
const cookies: { [key: string]: unknown } = {}
29+
return {
30+
get: (name: string) => {
31+
return cookies[name]
32+
},
33+
set: (name: string, value: string) => {
34+
cookies[name] = value
35+
},
36+
}
37+
},
38+
}
39+
})
40+
41+
let options = {
42+
redirectTo: "http://localhost/dashboard",
43+
email: "jane@example.com",
44+
}
45+
let authorizationParams = {}
46+
const testAdapter = TestAdapter()
47+
let config: NextAuthConfig = {
48+
secret: ["supersecret"],
49+
trustHost: true,
50+
basePath: "/api/auth",
51+
adapter: testAdapter,
52+
providers: [
53+
{
54+
id: "nodemailer",
55+
type: "email" as EmailProviderType,
56+
name: "Email",
57+
from: "no-reply@authjs.dev",
58+
maxAge: 86400,
59+
sendVerificationRequest: async () => { },
60+
options: {},
61+
},
62+
],
63+
}
64+
65+
describe("signIn", () => {
66+
beforeEach(() => {
67+
process.env.NEXTAUTH_URL = "http://localhost"
68+
})
69+
70+
afterEach(() => {
71+
process.env.NEXTAUTH_URL = undefined
72+
vi.resetModules()
73+
})
74+
75+
it("redirects to verify URL", async () => {
76+
await signIn("nodemailer", options, authorizationParams, config)
77+
78+
expect(mockRedirect).toHaveBeenCalledWith(
79+
"http://localhost/api/auth/verify-request?provider=nodemailer&type=email"
80+
)
81+
})
82+
83+
it("redirects to error URL", async () => {
84+
vi.doMock("@auth/core", async (importOriginal) => {
85+
const original = await importOriginal()
86+
return {
87+
// @ts-expect-error - not typed
88+
...original,
89+
Auth: () =>
90+
new Response(null, {
91+
headers: {
92+
Location: "http://localhost/api/auth/error?error=Configuration",
93+
},
94+
}),
95+
}
96+
})
97+
// Because we mock Auth we need to import dynamically
98+
// signIn from the module after mocking Auth
99+
const actionModule = await import("./actions")
100+
const signIn = actionModule.signIn
101+
let redirectTo: string | undefined | null
102+
redirectTo = await signIn(
103+
"nodemailer",
104+
{ ...options, redirect: false },
105+
authorizationParams,
106+
config
107+
)
108+
expect(redirectTo).toEqual(
109+
"http://localhost/api/auth/error?error=Configuration"
110+
)
111+
})
112+
})

0 commit comments

Comments
 (0)