Skip to content

Commit

Permalink
Fix NextAuth redirecting to /undefined when signIn method throws an
Browse files Browse the repository at this point in the history
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
  • Loading branch information
andresgutgon committed May 26, 2024
1 parent fc0e10a commit f9b1a41
Show file tree
Hide file tree
Showing 12 changed files with 347 additions and 6 deletions.
16 changes: 15 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,20 @@ 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.
throw error
}
}}
>
<button type="submit">
Expand Down
1 change: 1 addition & 0 deletions docs/pages/reference/_meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ export default {
"unstorage-adapter": "@auth/unstorage-adapter",
"upstash-redis-adapter": "@auth/upstash-redis-adapter",
"xata-adapter": "@auth/xata-adapter",
"test-adapter": "@auth/test-adapter",
}
25 changes: 25 additions & 0 deletions packages/adapter-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<p align="center">
<br/>
<a href="https://authjs.dev" target="_blank">
<img height="64px" src="https://authjs.dev/img/logo-sm.png" />
</a>
<h3 align="center"><b>Test Adapter</b> - NextAuth.js / Auth.js</a></h3>
<p align="center" style="align: center;">
<a href="https://npm.im/@auth/pg-adapter">
<img src="https://img.shields.io/badge/TypeScript-blue?style=flat-square" alt="TypeScript" />
</a>
<a href="https://npm.im/@auth/test-adapter">
<img alt="npm" src="https://img.shields.io/npm/v/@auth/pg-adapter?color=green&label=@auth/test-adapter&style=flat-square">
</a>
<a href="https://www.npmtrends.com/@auth/pg-adapter">
<img src="https://img.shields.io/npm/dm/@auth/test-adapter?label=%20downloads&style=flat-square" alt="Downloads" />
</a>
<a href="https://github.com/nextauthjs/next-auth/stargazers">
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth?style=flat-square" alt="GitHub Stars" />
</a>
</p>
</p>

---

Check out the documentation at [authjs.dev](https://authjs.dev/reference/adapter/test).
44 changes: 44 additions & 0 deletions packages/adapter-test/package.json
Original file line number Diff line number Diff line change
@@ -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 <hi@thvu.dev>"
],
"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:*"
}
}
110 changes: 110 additions & 0 deletions packages/adapter-test/src/index.ts
Original file line number Diff line number Diff line change
@@ -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<AdapterUser> & Pick<AdapterUser, "id">) {
if (user?.id !== data.id) {
throw new Error("No user id.")
}

user = { ...user, ...data }
return user
},
async updateSession(
data: Partial<AdapterSession> & Pick<AdapterSession, "sessionToken">
) {
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<AdapterAccount, "provider" | "providerAccountId">
) {
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<AdapterAccount, "provider" | "providerAccountId">
) {
if (
!account ||
account.provider !== params.provider ||
account.providerAccountId !== params.providerAccountId
) {
return undefined
}
account = null
},
}
}
9 changes: 9 additions & 0 deletions packages/adapter-test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "utils/tsconfig.json",
"compilerOptions": {
"outDir": ".",
"rootDir": "src"
},
"exclude": ["*.js", "*.d.ts"],
"include": ["src/**/*"]
}
14 changes: 14 additions & 0 deletions packages/adapter-test/typedoc.config.cjs
Original file line number Diff line number Diff line change
@@ -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',
}
4 changes: 3 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 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 Expand Up @@ -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"
Expand Down
109 changes: 109 additions & 0 deletions packages/next-auth/src/lib/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 { 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"
)
})
})
Loading

0 comments on commit f9b1a41

Please sign in to comment.