From f9b1a417fcebe1f90a9c843be1cb32bb754e55ba Mon Sep 17 00:00:00 2001
From: andresgutgon
Date: Sat, 25 May 2024 13:57:18 +0200
Subject: [PATCH] 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:
https://github.com/nextauthjs/next-auth/issues/11008
---
docs/pages/guides/pages/signin.mdx | 16 ++-
docs/pages/reference/_meta.js | 1 +
packages/adapter-test/README.md | 25 +++++
packages/adapter-test/package.json | 44 +++++++++
packages/adapter-test/src/index.ts | 110 +++++++++++++++++++++
packages/adapter-test/tsconfig.json | 9 ++
packages/adapter-test/typedoc.config.cjs | 14 +++
packages/core/src/index.ts | 4 +-
packages/next-auth/package.json | 2 +
packages/next-auth/src/lib/actions.test.ts | 109 ++++++++++++++++++++
packages/next-auth/src/lib/actions.ts | 10 +-
pnpm-lock.yaml | 9 ++
12 files changed, 347 insertions(+), 6 deletions(-)
create mode 100644 packages/adapter-test/README.md
create mode 100644 packages/adapter-test/package.json
create mode 100644 packages/adapter-test/src/index.ts
create mode 100644 packages/adapter-test/tsconfig.json
create mode 100644 packages/adapter-test/typedoc.config.cjs
create mode 100644 packages/next-auth/src/lib/actions.test.ts
diff --git a/docs/pages/guides/pages/signin.mdx b/docs/pages/guides/pages/signin.mdx
index 96e6113f1a..644ec9feb9 100644
--- a/docs/pages/guides/pages/signin.mdx
+++ b/docs/pages/guides/pages/signin.mdx
@@ -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 (
@@ -107,7 +108,20 @@ export default async function SignInPage() {
+
+---
+
+Check out the documentation at [authjs.dev](https://authjs.dev/reference/adapter/test).
diff --git a/packages/adapter-test/package.json b/packages/adapter-test/package.json
new file mode 100644
index 0000000000..4446b01883
--- /dev/null
+++ b/packages/adapter-test/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "@auth/test-adapter",
+ "version": "0.1.0",
+ "description": "Testing adapter for next-auth.",
+ "homepage": "https://authjs.dev",
+ "repository": "https://github.com/nextauthjs/next-auth",
+ "bugs": {
+ "url": "https://github.com/nextauthjs/next-auth/issues"
+ },
+ "author": "Jake Coppinger",
+ "contributors": [
+ "Thang Huu Vu "
+ ],
+ "license": "ISC",
+ "keywords": [
+ "next-auth",
+ "@auth",
+ "Auth.js",
+ "next.js",
+ "oauth"
+ ],
+ "type": "module",
+ "exports": {
+ ".": {
+ "types": "./index.d.ts",
+ "import": "./index.js"
+ }
+ },
+ "files": [
+ "*.d.ts*",
+ "*.js",
+ "src"
+ ],
+ "private": false,
+ "publishConfig": {
+ "access": "public"
+ },
+ "scripts": {
+ "build": "tsc"
+ },
+ "dependencies": {
+ "@auth/core": "workspace:*"
+ }
+}
diff --git a/packages/adapter-test/src/index.ts b/packages/adapter-test/src/index.ts
new file mode 100644
index 0000000000..4f5ea4d72c
--- /dev/null
+++ b/packages/adapter-test/src/index.ts
@@ -0,0 +1,110 @@
+import {
+ Adapter,
+ AdapterSession,
+ AdapterUser,
+ VerificationToken,
+ AdapterAccount,
+} from "@auth/core/adapters"
+
+export function TestAdapter(): Adapter {
+ let user: AdapterUser | null = null
+ let session: AdapterSession | null = null
+ let account: AdapterAccount | null = null
+ let verificationToken: VerificationToken | null = null
+ return {
+ async createUser(data: AdapterUser) {
+ user = data
+ return user
+ },
+ async getUser(userId: string) {
+ return user && user.id === userId ? user : null
+ },
+ async getUserByEmail(email: string) {
+ return user && user.email === email ? user : null
+ },
+ async createSession(data: {
+ sessionToken: string
+ userId: string
+ expires: Date
+ }) {
+ session = {
+ sessionToken: data.sessionToken,
+ userId: data.userId,
+ expires: data.expires,
+ }
+ return Promise.resolve(session)
+ },
+ async getSessionAndUser(sessionToken: string) {
+ const result =
+ session && session.sessionToken === sessionToken
+ ? { session: session!, user: user! }
+ : null
+ return Promise.resolve(result)
+ },
+ async updateUser(data: Partial & Pick) {
+ if (user?.id !== data.id) {
+ throw new Error("No user id.")
+ }
+
+ user = { ...user, ...data }
+ return user
+ },
+ async updateSession(
+ data: Partial & Pick
+ ) {
+ if (!session || session.sessionToken !== data.sessionToken) return null
+
+ session = { ...session, ...data }
+ return session
+ },
+ async linkAccount(data: AdapterAccount) {
+ account = data
+ return Promise.resolve(account)
+ },
+ async getUserByAccount(
+ input: Pick
+ ) {
+ return account &&
+ account.provider === input.provider &&
+ account.providerAccountId === input.providerAccountId
+ ? user
+ : null
+ },
+ async deleteSession(sessionToken: string) {
+ if (!session || session.sessionToken !== sessionToken) return null
+ session = null
+ },
+ async createVerificationToken(data: VerificationToken) {
+ verificationToken = data
+ return verificationToken
+ },
+ async useVerificationToken(params: { identifier: string; token: string }) {
+ if (!verificationToken) return null
+ if (
+ verificationToken.identifier === params.identifier &&
+ verificationToken.token === params.token
+ ) {
+ verificationToken = null
+ return verificationToken
+ }
+
+ return null
+ },
+ async deleteUser(id: string) {
+ if (user?.id !== id) return undefined
+ user = null
+ },
+ async unlinkAccount(
+ params: Pick
+ ) {
+ if (
+ !account ||
+ account.provider !== params.provider ||
+ account.providerAccountId !== params.providerAccountId
+ ) {
+ return undefined
+ }
+ account = null
+ },
+ }
+}
diff --git a/packages/adapter-test/tsconfig.json b/packages/adapter-test/tsconfig.json
new file mode 100644
index 0000000000..df8621c516
--- /dev/null
+++ b/packages/adapter-test/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "utils/tsconfig.json",
+ "compilerOptions": {
+ "outDir": ".",
+ "rootDir": "src"
+ },
+ "exclude": ["*.js", "*.d.ts"],
+ "include": ["src/**/*"]
+}
diff --git a/packages/adapter-test/typedoc.config.cjs b/packages/adapter-test/typedoc.config.cjs
new file mode 100644
index 0000000000..b5103c3a1b
--- /dev/null
+++ b/packages/adapter-test/typedoc.config.cjs
@@ -0,0 +1,14 @@
+// @ts-check
+
+/**
+ * @type {import('typedoc').TypeDocOptions & import('typedoc-plugin-markdown').MarkdownTheme}
+ */
+module.exports = {
+ entryPoints: ["src/index.ts"],
+ entryPointStrategy: "expand",
+ tsconfig: "./tsconfig.json",
+ entryModule: "@auth/test-adapter",
+ entryFileName: "../test-adapter.mdx",
+ includeVersion: true,
+ readme: 'none',
+}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index f88617a9d3..a7ce393ab6 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -170,7 +170,9 @@ export async function Auth(
return Response.json({ url }, { headers: response.headers })
} catch (e) {
const error = e as Error
- logger.error(error)
+ if (process.env.NODE_ENV !== "test") {
+ logger.error(error)
+ }
const isAuthError = error instanceof AuthError
if (isAuthError && isRaw && !isRedirect) throw error
diff --git a/packages/next-auth/package.json b/packages/next-auth/package.json
index eeb2b65098..72dde01969 100644
--- a/packages/next-auth/package.json
+++ b/packages/next-auth/package.json
@@ -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": [
@@ -99,6 +100,7 @@
},
"devDependencies": {
"@types/react": "18.0.37",
+ "@auth/test-adapter": "workspace:*",
"next": "14.0.3-canary.1",
"nodemailer": "^6.9.3",
"react": "^18.2.0"
diff --git a/packages/next-auth/src/lib/actions.test.ts b/packages/next-auth/src/lib/actions.test.ts
new file mode 100644
index 0000000000..b98ef0fadb
--- /dev/null
+++ b/packages/next-auth/src/lib/actions.test.ts
@@ -0,0 +1,109 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
+import { signIn } from "./actions"
+import { EmailProviderType } from "../providers"
+import { TestAdapter } from "@auth/test-adapter"
+import { NextAuthConfig } from "."
+
+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
+ },
+ }
+ },
+ }
+})
+
+let options = {
+ redirectTo: "http://localhost/dashboard",
+ email: "jane@example.com",
+}
+let authorizationParams = {}
+let config: NextAuthConfig = {
+ secret: ["supersecret"],
+ trustHost: true,
+ basePath: "/api/auth",
+ adapter: TestAdapter(),
+ providers: [
+ {
+ id: "nodemailer",
+ type: "email" as EmailProviderType,
+ name: "Email",
+ from: "no-reply@authjs.dev",
+ maxAge: 86400,
+ sendVerificationRequest: async () => { },
+ options: {},
+ },
+ ],
+}
+
+describe("signIn", () => {
+ beforeEach(() => {
+ process.env.NEXTAUTH_URL = "http://localhost"
+ })
+
+ afterEach(() => {
+ process.env.NEXTAUTH_URL = undefined
+ })
+
+ it("redirects to verify URL", async () => {
+ await signIn("nodemailer", options, authorizationParams, config)
+
+ expect(mockRedirect).toHaveBeenCalledWith(
+ "http://localhost/api/auth/verify-request?provider=nodemailer&type=email"
+ )
+ })
+
+ it("redirects to error URL", async () => {
+ config = {
+ ...config,
+ providers: [
+ {
+ id: "nodemailer",
+ type: "email" as EmailProviderType,
+ name: "Email",
+ from: "no-reply@authjs.dev",
+ maxAge: 86400,
+ sendVerificationRequest: async () => {
+ throw new Error("IGNORE_ERROR_IN_TEST_STACK")
+ },
+ options: {},
+ },
+ ],
+ }
+ let redirectTo: string | undefined | null
+ redirectTo = await signIn(
+ "nodemailer",
+ { ...options, redirect: false },
+ authorizationParams,
+ config
+ )
+ expect(redirectTo).toEqual(
+ "http://localhost/api/auth/error?error=Configuration"
+ )
+ })
+})
diff --git a/packages/next-auth/src/lib/actions.ts b/packages/next-auth/src/lib/actions.ts
index 77b56667a8..e197bf29f2 100644
--- a/packages/next-auth/src/lib/actions.ts
+++ b/packages/next-auth/src/lib/actions.ts
@@ -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"
type SignInParams = Parameters
export async function signIn(
@@ -73,8 +71,12 @@ 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
+
+ if (shouldRedirect) return redirect(responseUrl!)
+
+ return responseUrl as any
}
type SignOutParams = Parameters
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 101347e8d0..575ee44bea 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -555,6 +555,12 @@ importers:
specifier: ^0.11.0
version: 0.11.0
+ packages/adapter-test:
+ dependencies:
+ '@auth/core':
+ specifier: workspace:*
+ version: link:../core
+
packages/adapter-typeorm:
dependencies:
'@auth/core':
@@ -782,6 +788,9 @@ importers:
specifier: ^9.0.2
version: 9.0.2(encoding@0.1.13)
devDependencies:
+ '@auth/test-adapter':
+ specifier: workspace:*
+ version: link:../adapter-test
'@types/react':
specifier: 18.0.37
version: 18.0.37