Compare commits

...

128 Commits

Author SHA1 Message Date
ndom91
657dd57ef7 fix(docs): clean up v5 migration intro 2023-05-07 14:30:01 +02:00
ndom91
39691313fb feat(docs): initial v5 next-auth upgrade guide doc 2023-05-07 14:17:32 +02:00
Balázs Orbán
a7e02266cf fix value 2023-05-05 16:14:16 +02:00
Balázs Orbán
bac8b8d13a add getServerSession, simplify auth() return 2023-05-05 14:33:45 +02:00
Balázs Orbán
bb4d5ce29b update dev app 2023-05-05 14:32:40 +02:00
Balázs Orbán
436d46ab89 tweak docs 2023-05-05 02:57:19 +02:00
Balázs Orbán
b17dc76a70 Merge branch 'main' into feat/nextjs-auth 2023-05-05 02:38:57 +02:00
Balázs Orbán
b96f01319c chore: tweak manual release version 2023-05-05 02:38:47 +02:00
Balázs Orbán
5fb4caa485 whitespace 2023-05-05 02:32:04 +02:00
Balázs Orbán
24e8100a61 move 2023-05-05 02:31:52 +02:00
Balázs Orbán
b17c5deafb add installation 2023-05-05 02:31:40 +02:00
Balázs Orbán
7da32563d6 docs 2023-05-05 02:30:33 +02:00
Balázs Orbán
c470960d93 docs 2023-05-05 02:23:00 +02:00
Balázs Orbán
8cfd1aeb15 improve docs 2023-05-05 02:20:41 +02:00
Balázs Orbán
af81776e4b upgrade dev app 2023-05-05 01:57:23 +02:00
Balázs Orbán
3ba510c0e0 forward all set-cookie headers 2023-05-05 01:57:00 +02:00
Balázs Orbán
65fe0ef363 fix imports 2023-05-05 00:14:16 +02:00
Balázs Orbán
587d3666fc Merge branch 'main' into feat/nextjs-auth 2023-05-04 23:59:06 +02:00
Balázs Orbán
8f416b68ec chore: tweaks 2023-05-04 23:58:54 +02:00
Balázs Orbán
e4d568c220 fix redirects 2023-05-04 23:55:36 +02:00
Balázs Orbán
aaff906c68 add readme 2023-05-04 23:52:39 +02:00
Balázs Orbán
8f40883de5 Merge branch 'main' into feat/nextjs-auth 2023-05-04 23:51:00 +02:00
Balázs Orbán
eaf5080721 chore: tweak 2023-05-04 23:50:49 +02:00
Balázs Orbán
601c8c915f Merge branch 'main' into feat/nextjs-auth 2023-05-04 23:42:20 +02:00
Balázs Orbán
e0b5f18c5b chore: skip test for manual release 2023-05-04 23:42:05 +02:00
Balázs Orbán
7fae8744b2 Merge branch 'main' into feat/nextjs-auth 2023-05-04 23:41:33 +02:00
Balázs Orbán
99247ce446 chore: separate manual release job 2023-05-04 23:40:37 +02:00
Balázs Orbán
0afb1797ba Merge branch 'main' into feat/nextjs-auth 2023-05-04 23:25:39 +02:00
Balázs Orbán
d6bc65f0d8 chore: support release any package as experimental 2023-05-04 23:25:29 +02:00
Balázs Orbán
0b80c65a0c update lock file 2023-05-04 22:45:58 +02:00
Balázs Orbán
a7991aed15 revert 2023-05-04 22:38:54 +02:00
Balázs Orbán
c47a2eb7ac Merge branch 'main' into feat/nextjs-auth 2023-05-04 22:22:00 +02:00
Balázs Orbán
6f5a50313f chore: use @ts-ignore 2023-05-04 22:21:36 +02:00
Balázs Orbán
3c23a4f4e2 add docs 2023-05-04 22:14:15 +02:00
Balázs Orbán
52c04fe89e update turbo 2023-05-04 22:11:43 +02:00
Balázs Orbán
440317aa2f update turbo 2023-05-04 22:10:39 +02:00
Balázs Orbán
18a4e8b255 update turbo 2023-05-04 22:09:20 +02:00
Balázs Orbán
c411c6f5dc update gitignore 2023-05-04 22:06:08 +02:00
Balázs Orbán
46f622a5b4 Merge branch 'main' into feat/nextjs-auth 2023-05-04 22:05:27 +02:00
Balázs Orbán
1b984f09ab fix docs references 2023-05-04 21:51:11 +02:00
Balázs Orbán
fbc1ed857c space 2023-05-04 21:43:43 +02:00
Balázs Orbán
0d6ad6beb6 update lock file 2023-05-04 21:43:15 +02:00
Balázs Orbán
b972742171 Update settings.json 2023-05-04 20:42:31 +01:00
Balázs Orbán
01d6f86538 Update .prettierignore 2023-05-04 20:42:16 +01:00
Balázs Orbán
bf89d9fabb re-add next-auth 4 2023-05-04 21:41:36 +02:00
Balázs Orbán
90b01c4613 Merge branch 'feat/nextjs-auth' of github.com:nextauthjs/next-auth into feat/nextjs-auth 2023-05-04 21:39:40 +02:00
Balázs Orbán
47794e68cc move back to @auth/nextjs 2023-05-04 21:39:37 +02:00
Balázs Orbán
0c6b8581a7 revert 2023-05-04 20:30:08 +01:00
Balázs Orbán
7ece5a0a9d chore: fix framework builds 2023-05-04 21:26:53 +02:00
Balázs Orbán
828e38c976 ignore type errors 2023-05-04 21:19:11 +02:00
Balázs Orbán
47d4a3f80f mark import as type 2023-05-04 20:52:20 +02:00
Balázs Orbán
14f9388148 remove unnecessary type import 2023-05-04 20:30:23 +02:00
Balázs Orbán
46659f04b7 update deps 2023-05-04 19:55:45 +02:00
Balázs Orbán
fc4abd174f Merge branch 'main' into feat/nextjs-auth 2023-05-04 10:58:03 +02:00
Balázs Orbán
ebefd25bd6 improve docs 2023-05-04 10:57:42 +02:00
Balázs Orbán
e3bdb38df2 fix(docs): remove extra heading
Fixes #7426
2023-05-03 12:40:30 +02:00
Balázs Orbán
da9ce95677 add redirects 2023-05-01 14:18:56 +02:00
Balázs Orbán
8c99f5c9cf Merge branch 'main' into feat/nextjs-auth 2023-05-01 13:49:29 +02:00
Balázs Orbán
92a0fc42fa fix: allow handling OAuth callback error response
related #7407
2023-05-01 13:49:17 +02:00
Balázs Orbán
62e2ad115c chore: type fixes 2023-05-01 13:46:23 +02:00
Balázs Orbán
5551c4f147 add more providers 2023-05-01 13:42:35 +02:00
Balázs Orbán
e2ef07688c Merge branch 'main' into feat/nextjs-auth 2023-05-01 13:22:31 +02:00
Balázs Orbán
542c35d729 fix: loosen profile types 2023-05-01 13:22:16 +02:00
Balázs Orbán
e62fa3a0af infer issuer 2023-05-01 13:21:42 +02:00
Balázs Orbán
771e993ce9 fix import 2023-05-01 13:03:49 +02:00
Balázs Orbán
c4a15ae3dd bump versions 2023-05-01 12:56:32 +02:00
Balázs Orbán
80d1a8fd70 mention NA5 status in docs 2023-05-01 12:54:56 +02:00
Balázs Orbán
54876744fe Merge branch 'feat/nextjs-auth' into feat/nextjs-auth-example-app 2023-05-01 12:52:34 +02:00
Balázs Orbán
28cbb3aac5 rename @auth/nextjs to next-auth 2023-05-01 12:41:12 +02:00
Balázs Orbán
ac0ca51278 Merge branch 'main' into feat/nextjs-auth 2023-05-01 11:32:56 +02:00
Balázs Orbán
5400645221 chore: improve errors, add more docs (#7415)
* JWT Token -> JWT

* document some errors

* improve errors, docs
2023-05-01 10:32:20 +01:00
Balázs Orbán
d739e8e04e feat(adapters): add Account mapping before database write (#7369)
* feat: map Account before saving to database

* document `acconut()`, explain default behaviour

* generate `expires_at` based on `expires_in`

Fixes #6538

* rename

* strip undefined on `defaultProfile`

* don't forward defaults to account callback

* improve internal namings, types, docs
2023-04-30 12:25:26 +01:00
Victor
c2eb9b3ad4 fix(docs): fix default maxAge formula (#7406) 2023-04-30 10:34:05 +02:00
Balázs Orbán
4f54840014 bump to next@canary 2023-04-28 12:59:48 +02:00
Balázs Orbán
d938333750 Merge branch 'main' into feat/nextjs-auth 2023-04-28 12:29:49 +02:00
ndom91
d837dfaea1 fix: app router style updates for example app 2023-04-27 14:17:03 +02:00
ndom91
a99f4bd8c6 fix: remove dependency on 'next-auth' from example package.json 2023-04-27 13:39:06 +02:00
Balázs Orbán
a96b8597b1 Merge branch 'main' into feat/nextjs-auth 2023-04-27 11:07:20 +02:00
Balázs Orbán
29ce4cb4d4 fix imports 2023-04-27 10:58:02 +02:00
Balázs Orbán
da40242e48 bump next 2023-04-27 10:57:56 +02:00
Balázs Orbán
3f211e7ad0 Merge branch 'main' into feat/nextjs-auth 2023-04-27 10:14:56 +02:00
ndom91
5f369b0981 fix: update README.md to '@auth/nextjs' 2023-04-26 16:25:52 +02:00
ndom91
4a056c774f fix: rm 'pages' dir 2023-04-26 16:25:41 +02:00
ndom91
038b9bccad fix: remove unnecessary header links in example app 2023-04-25 19:05:33 +02:00
ndom91
dfb20849c5 fix: pages/server page on dev and example app 2023-04-25 19:05:11 +02:00
ndom91
4a7a7ab757 fix: minor .env.local.example update 2023-04-25 18:44:05 +02:00
ndom91
f94b604397 fix: move pages to backup position in example app 2023-04-25 18:41:28 +02:00
ndom91
535f9276f6 feat: move nextjs example app to new primitives 2023-04-25 18:37:23 +02:00
Balázs Orbán
cd2872de89 Merge branch 'main' into feat/nextjs-auth 2023-04-24 12:00:11 +03:00
Balázs Orbán
2de2dc9bb3 set version 2023-04-21 12:38:15 +02:00
Balázs Orbán
6f96004d75 tweak docs 2023-04-21 12:37:21 +02:00
Balázs Orbán
f834bc2a99 mark getCsrfToken as internal 2023-04-21 12:35:19 +02:00
Balázs Orbán
551dcbd2d5 show client api in sidebar 2023-04-21 12:34:04 +02:00
Balázs Orbán
316b344930 remove gitignored file 2023-04-21 12:31:07 +02:00
Balázs Orbán
97ff6406cb Merge branch 'main' into feat/nextjs-auth 2023-04-21 12:30:48 +02:00
Balázs Orbán
1b9559fbb1 add docs, fix types 2023-04-21 13:21:55 +03:00
Balázs Orbán
27864eea2f handle JSX 2023-04-21 11:46:16 +02:00
Balázs Orbán
e5f18e3266 add @auth/nextjs/client 2023-04-21 11:46:08 +02:00
Balázs Orbán
47eec2c498 update pages 2023-04-21 11:45:05 +02:00
Balázs Orbán
77bba4ada7 App Router updates 2023-04-21 11:40:46 +02:00
Balázs Orbán
f081fcd31d remove supabase pages 2023-04-21 11:39:17 +02:00
Balázs Orbán
5f079930cc update imports 2023-04-21 11:38:45 +02:00
Balázs Orbán
4cc4b45e52 bump package versions 2023-04-21 11:37:37 +02:00
Balázs Orbán
c0cfb13c88 Merge branch 'main' into feat/nextjs-auth 2023-04-20 11:51:37 +02:00
Balázs Orbán
537112a306 Merge branch 'main' into feat/nextjs-auth 2023-04-19 10:57:14 +02:00
Balázs Orbán
6663003c7c remove crypto polyfill 2023-04-18 13:57:16 +02:00
Balázs Orbán
68559941a5 support __Secure session token cookie 2023-04-18 13:50:47 +02:00
Balázs Orbán
cec1fd753e ignore next-2 2023-04-18 10:40:26 +02:00
Balázs Orbán
c97c40c9cb fix types, default envs 2023-04-18 10:29:06 +02:00
Balázs Orbán
defc2233be update session expiry 2023-04-14 13:50:12 +02:00
Balázs Orbán
e0450c9d52 remove log 2023-04-14 12:19:28 +02:00
Balázs Orbán
04021c6d47 respect AUTH_SECRET 2023-04-14 12:14:17 +02:00
Balázs Orbán
210d28b6b0 add lib to package files 2023-04-14 12:14:07 +02:00
Balázs Orbán
fcd9bfc6f8 update node types 2023-04-14 12:01:56 +02:00
Balázs Orbán
0a027cf35d update deps/lock file 2023-04-14 11:58:32 +02:00
Balázs Orbán
47ac2e94ce document source code 2023-04-14 11:43:26 +02:00
Balázs Orbán
30e3672708 generate docs 2023-04-14 11:42:47 +02:00
Balázs Orbán
635a9b0c50 update dev app 2023-04-14 11:41:15 +02:00
Balázs Orbán
510b9764f5 add session rotation 2023-04-13 14:44:06 +02:00
Balázs Orbán
7329725702 update deps 2023-04-13 14:43:48 +02:00
Balázs Orbán
23ea9428e0 update dev app 2023-04-13 14:43:25 +02:00
Balázs Orbán
319f7af866 Merge branch 'main' into feat/nextjs-auth 2023-04-12 12:41:41 +02:00
Balázs Orbán
58be169b10 update dev app 2023-04-07 12:51:41 +02:00
Balázs Orbán
c87fdd9060 add implicit env reading, simplified API 2023-04-07 12:51:34 +02:00
Balázs Orbán
28263f52bd Merge branch 'main' into feat/nextjs-auth 2023-04-07 12:39:10 +02:00
Balázs Orbán
27869b70b8 Merge branch 'main' into feat/nextjs-auth 2023-04-01 14:39:34 +02:00
Balázs Orbán
7af7ca4d1c chore: bump turbo and pnpm 2023-03-31 15:50:44 +02:00
Balázs Orbán
30948fbada chore: update configs, dependencies 2023-03-30 18:13:07 +02:00
117 changed files with 3019 additions and 1413 deletions

View File

@@ -5,14 +5,15 @@ const core = require("@actions/core")
try {
const packageJSONPath = path.join(
process.cwd(),
"packages/next-auth/package.json"
`packages/${process.env.PACKAGE_PATH || "next-auth"}/package.json`
)
const packageJSON = JSON.parse(fs.readFileSync(packageJSONPath, "utf8"))
const sha8 = process.env.GITHUB_SHA.substring(0, 8)
const prNumber = process.env.PR_NUMBER
const packageVersion = `0.0.0-pr.${prNumber}.${sha8}`
const prefix = "0.0.0-"
const pr = process.env.PR_NUMBER
const source = pr ? `pr.${pr}` : "manual"
const packageVersion = `${prefix}${source}.${sha8}`
packageJSON.version = packageVersion
core.setOutput("version", packageVersion)
fs.writeFileSync(packageJSONPath, JSON.stringify(packageJSON))

View File

@@ -8,6 +8,24 @@ on:
- next
- 3.x
pull_request:
# TODO: Support latest releases
workflow_dispatch:
inputs:
name:
type: choice
description: Package name (npm)
options:
- "@auth/nextjs"
- "@auth/core"
- "next-auth"
# TODO: Infer from package name
path:
type: choice
description: Directory name (packages/*)
options:
- "frameworks-nextjs"
- "core"
- "next-auth"
jobs:
test:
@@ -122,3 +140,34 @@ jobs:
env:
VERSION: ${{ steps.determine-version.outputs.version }}
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
release-manual:
name: Publish manually
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }}
steps:
- name: Init
uses: actions/checkout@v3
- name: Install pnpm
uses: pnpm/action-setup@v2.2.4
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: pnpm install
- name: Determine version
uses: ./.github/version-pr
id: determine-version
env:
PACKAGE_PATH: ${{ github.event.inputs.path }}
- name: Publish to npm
run: |
cd packages/$PACKAGE_PATH
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc
pnpm publish --no-git-checks --access public --tag experimental
echo "🎉 Experimental release published 📦️ on npm: https://npmjs.com/package/${{ github.event.inputs.name }}/v/${{ env.VERSION }}"
echo "Install via: pnpm add ${{ github.event.inputs.name }}@${{ env.VERSION }}"
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
PACKAGE_PATH: ${{ github.event.inputs.path }}
VERSION: ${{ steps.determine-version.outputs.version }}

3
.gitignore vendored
View File

@@ -85,7 +85,10 @@ packages/core/providers
packages/core/src/lib/pages/styles.ts
docs/docs/reference/core
docs/docs/reference/sveltekit
docs/docs/reference/nextjs
# Next.js
packages/frameworks-nextjs/lib
# SvelteKit
packages/frameworks-sveltekit/index.*

View File

@@ -0,0 +1,3 @@
import { handlers } from "auth"
export const { GET, POST } = handlers
export const runtime = "edge"

View File

@@ -0,0 +1,10 @@
import { auth } from "auth"
import { NextResponse } from "next/server"
export const GET = auth(function GET(req) {
if (req.auth) {
return NextResponse.json(req.auth)
}
return NextResponse.json({ message: "Not authenticated" }, { status: 401 })
})

View File

@@ -0,0 +1,10 @@
"use client"
import { useEffect } from "react"
export default function Client({ session }: any) {
useEffect(() => {
console.log(window.location)
})
return <div>{JSON.stringify(session)}</div>
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <h1>This page is protected.</h1>
}

View File

@@ -1,12 +1,54 @@
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
import { auth } from "auth"
import Footer from "components/footer"
import { Header } from "components/header"
import { cookies, headers } from "next/headers"
import styles from "components/header.module.css"
import "./styles.css"
export default function RootLayout(props: { children: React.ReactNode }) {
return (
<html>
<head></head>
<body>{children}</body>
<body>
{/* @ts-expect-error */}
<AppHeader />
<main>{props.children}</main>
<Footer />
</body>
</html>
)
}
function SignIn({ id, ...props }: any) {
const $cookies = cookies()
const csrfToken = $cookies.get("next-auth.csrf-token")?.value.split("|")[0]
const action = id ? `/api/auth/signin/${id}` : "/api/auth/signin"
return (
<form action={action} method="post">
<button {...props} />
<input type="hidden" name="csrfToken" value={csrfToken} />
</form>
)
}
function SignOut(props: any) {
const $cookies = cookies()
const csrfToken = $cookies.get("next-auth.csrf-token")?.value.split("|")[0]
return (
<form action="/api/auth/signout" method="post">
<button {...props} />
<input type="hidden" name="csrfToken" value={csrfToken} />
</form>
)
}
export async function AppHeader() {
const session = await auth(headers())
return (
<Header
session={session}
signIn={<SignIn className={styles.buttonPrimary}>Sign in</SignIn>}
signOut={<SignOut className={styles.button}>Sign out</SignOut>}
/>
)
}

View File

@@ -0,0 +1,17 @@
import { auth } from "auth"
import { headers } from "next/headers"
import Client from "./client"
export default async function Page() {
const session = await auth(headers())
return (
<>
<Client session={session} />
<h1>NextAuth.js Example</h1>
<p>
This is an example site to demonstrate how to use{" "}
<a href="https://nextjs.authjs.dev">NextAuth.js</a> for authentication.
</p>
</>
)
}

View File

@@ -1,6 +0,0 @@
import { unstable_getServerSession } from "next-auth/next"
export default async function Page() {
const session = await unstable_getServerSession()
return <pre>{JSON.stringify(session, null, 2)}</pre>
}

View File

@@ -6,7 +6,7 @@ body {
max-width: 680px;
margin: 0 auto;
background: #fff;
color: #333;
color: var(--color-text);
}
li,

31
apps/dev/nextjs/auth.ts Normal file
View File

@@ -0,0 +1,31 @@
import NextAuth from "@auth/nextjs"
import Auth0 from "@auth/core/providers/auth0"
import Facebook from "@auth/core/providers/facebook"
import GitHub from "@auth/core/providers/github"
import Google from "@auth/core/providers/google"
import Twitter from "@auth/core/providers/twitter"
import Credentials from "@auth/core/providers/credentials"
export const { handlers, auth, getServerSession } = NextAuth({
debug: true,
providers: [
GitHub,
Auth0,
Facebook,
Google,
Twitter,
Credentials({
credentials: { password: { label: "Password", type: "password" } },
authorize(c) {
if (c.password !== "password") return null
return { id: "test", name: "Test User", email: "test@example.com" }
},
}),
],
callbacks: {
async authorized({ request: { nextUrl }, auth }) {
if (nextUrl.pathname === "/dashboard") return !!auth.user
return true
},
},
})

View File

@@ -1,4 +1,4 @@
import { signIn } from "next-auth/react"
import { signIn } from "@auth/nextjs/react"
export default function AccessDenied() {
return (

View File

@@ -1,6 +1,6 @@
import Link from "next/link"
import styles from "./footer.module.css"
import packageJSON from "package.json"
import packageJSON from "@auth/nextjs/package.json"
export default function Footer() {
return (

View File

@@ -1,89 +0,0 @@
import Link from "next/link"
import { useSession } from "next-auth/react"
import styles from "./header.module.css"
// The approach used in this component shows how to built a sign in and sign out
// component that works on pages which support both client and server side
// rendering, and avoids any flash incorrect content on initial page load.
export default function Header() {
const { data: session, status } = useSession()
return (
<header>
<noscript>
<style>{".nojs-show { opacity: 1; top: 0; }"}</style>
</noscript>
<div className={styles.signedInStatus}>
<p
className={`nojs-show ${
!session && status === "loading" ? styles.loading : styles.loaded
}`}
>
{!session && (
<>
<span className={styles.notSignedInText}>
You are not signed in
</span>
<a href="/api/auth/signin" className={styles.buttonPrimary}>
Sign in
</a>
</>
)}
{session && (
<>
{session.user.image && (
<img src={session.user.image} className={styles.avatar} />
)}
<span className={styles.signedInText}>
<small>Signed in as</small>
<br />
<strong>{session.user.email} </strong>
{session.user.name ? `(${session.user.name})` : null}
</span>
<a href="/api/auth/signout" className={styles.button}>
Sign out
</a>
</>
)}
</p>
</div>
<nav>
<ul className={styles.navItems}>
<li className={styles.navItem}>
<Link href="/">Home</Link>
</li>
<li className={styles.navItem}>
<Link href="/client">Client</Link>
</li>
<li className={styles.navItem}>
<Link href="/server">Server</Link>
</li>
<li className={styles.navItem}>
<Link href="/protected">Protected</Link>
</li>
<li className={styles.navItem}>
<Link href="/protected-ssr">Protected(SSR)</Link>
</li>
<li className={styles.navItem}>
<Link href="/api-example">API</Link>
</li>
<li className={styles.navItem}>
<Link href="/credentials">Credentials</Link>
</li>
<li className={styles.navItem}>
<Link href="/email">Email</Link>
</li>
<li className={styles.navItem}>
<Link href="/middleware-protected">Middleware protected</Link>
</li>
<li className={styles.navItem}>
<Link href="/supabase-client-rls">Supabase RLS</Link>
</li>
<li className={styles.navItem}>
<Link href="/supabase-ssr">Supabase RLS(SSR)</Link>
</li>
</ul>
</nav>
</header>
)
}

View File

@@ -1,6 +1,7 @@
/* Set min-height to avoid page reflow while session loading */
.signedInStatus {
display: block;
display: flex;
align-items: center;
min-height: 4rem;
width: 100%;
}
@@ -25,16 +26,13 @@
.signedInText,
.notSignedInText {
position: absolute;
padding-top: 0.8rem;
left: 1rem;
right: 6.5rem;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
display: inherit;
z-index: 1;
line-height: 1.3rem;
flex: 1;
}
.signedInText {
@@ -47,6 +45,7 @@
float: left;
height: 2.8rem;
width: 2.8rem;
margin-right: 1rem;
background-color: white;
background-size: cover;
background-repeat: no-repeat;
@@ -54,10 +53,11 @@
.button,
.buttonPrimary {
float: right;
margin-right: -0.4rem;
justify-self: end;
font-weight: 500;
border-radius: 0.3rem;
border: none;
font-weight: bold;
cursor: pointer;
font-size: 1rem;
line-height: 1.4rem;

View File

@@ -0,0 +1,55 @@
import Link from "next/link"
import styles from "./header.module.css"
export function Header({ session, signIn, signOut }: any) {
return (
<header>
<div className={styles.signedInStatus}>
{!session && (
<>
<span className={styles.notSignedInText}>
You are not signed in
</span>
{signIn}
</>
)}
{session && (
<>
{session.user.picture && (
<img src={session.user.picture} className={styles.avatar} />
)}
<span className={styles.signedInText}>
<small>Signed in as</small>
<br />
<strong>{session.user.email} </strong>
{session.user.name ? `(${session.user.name})` : null}
</span>
{signOut}
</>
)}
</div>
<nav>
<ul className={styles.navItems}>
<li className={styles.navItem}>
<Link href="/">Home (app)</Link>
</li>
<li className={styles.navItem}>
<Link href="/dashboard">Dashboard (app)</Link>
</li>
<li className={styles.navItem}>
<Link href="/policy">Policy (pages)</Link>
</li>
<li className={styles.navItem}>
<Link href="/credentials">Credentials (pages)</Link>
</li>
<li className={styles.navItem}>
<Link href="/protected-ssr">getServerSideProps (pages)</Link>
</li>
<li className={styles.navItem}>
<Link href="/api/examples/protected">API Route (pages)</Link>
</li>
</ul>
</nav>
</header>
)
}

View File

@@ -1,12 +0,0 @@
import Header from "components/header"
import Footer from "components/footer"
export default function Layout({ children }) {
return (
<>
<Header />
<main>{children}</main>
<Footer />
</>
)
}

View File

@@ -1,45 +1 @@
export { default } from "next-auth/middleware"
export const config = { matcher: ["/middleware-protected"] }
// Other ways to use this middleware
// import withAuth from "next-auth/middleware"
// import { withAuth } from "next-auth/middleware"
// export function middleware(req, ev) {
// return withAuth(req)
// }
// export function middleware(req, ev) {
// return withAuth(req, ev)
// }
// export function middleware(req, ev) {
// return withAuth(req, {
// callbacks: {
// authorized: ({ token }) => !!token,
// },
// })
// }
// export default withAuth(function middleware(req, ev) {
// console.log(req.nextauth.token)
// })
// export default withAuth(
// function middleware(req, ev) {
// console.log(req, ev)
// },
// {
// callbacks: {
// authorized: ({ token }) => token.name === "Balázs Orbán",
// },
// }
// )
// export default withAuth({
// callbacks: {
// authorized: ({ token }) => !!token,
// },
// })
export { auth as default } from "auth"

View File

@@ -15,6 +15,7 @@
"license": "ISC",
"dependencies": {
"@auth/core": "workspace:*",
"@auth/nextjs": "workspace:*",
"@next-auth/fauna-adapter": "workspace:*",
"@next-auth/prisma-adapter": "workspace:*",
"@next-auth/supabase-adapter": "workspace:*",
@@ -22,7 +23,7 @@
"@prisma/client": "^3",
"@supabase/supabase-js": "^2.0.5",
"faunadb": "^4",
"next": "13.3.0",
"next": "13.4.0",
"next-auth": "workspace:*",
"nodemailer": "^6",
"react": "^18",

View File

@@ -1,10 +0,0 @@
import { SessionProvider } from "next-auth/react"
import "./styles.css"
export default function App({ Component, pageProps }) {
return (
<SessionProvider session={pageProps.session}>
<Component {...pageProps} />
</SessionProvider>
)
}

View File

@@ -0,0 +1,39 @@
import {
SessionProvider,
signIn,
signOut,
useSession,
} from "@auth/nextjs/react"
import "./styles.css"
import { Header } from "components/header"
import styles from "components/header.module.css"
import Footer from "components/footer"
export default function App({ Component, pageProps }) {
return (
<SessionProvider session={pageProps.session}>
<PagesHeader />
<Component {...pageProps} />
<Footer />
</SessionProvider>
)
}
function PagesHeader() {
const { data: session } = useSession()
return (
<Header
session={session}
signIn={
<button onClick={() => signIn()} className={styles.buttonPrimary}>
Sign in
</button>
}
signOut={
<button onClick={() => signOut()} className={styles.button}>
Sign out
</button>
}
/>
)
}

View File

@@ -94,7 +94,11 @@ export const authConfig: AuthConfig = {
tenantId: process.env.AZURE_AD_TENANT_ID,
}),
AzureB2C({ clientId: process.env.AZURE_B2C_ID, clientSecret: process.env.AZURE_B2C_SECRET, issuer: process.env.AZURE_B2C_ISSUER }),
BeyondIdentity({ clientId: process.env.BEYOND_IDENTITY_CLIENT_ID, clientSecret: process.env.BEYOND_IDENTITY_CLIENT_SECRET, issuer: process.env.BEYOND_IDENTITY_ISSUER }),
BeyondIdentity({
clientId: process.env.BEYOND_IDENTITY_CLIENT_ID,
clientSecret: process.env.BEYOND_IDENTITY_CLIENT_SECRET,
issuer: process.env.BEYOND_IDENTITY_ISSUER,
}),
BoxyHQSAML({ issuer: "https://jackson-demo.boxyhq.com", clientId: "tenant=boxyhq.com&product=saml-demo.boxyhq.com", clientSecret: "dummy" }),
// Cognito({ clientId: process.env.COGNITO_ID, clientSecret: process.env.COGNITO_SECRET, issuer: process.env.COGNITO_ISSUER }),
Discord({ clientId: process.env.DISCORD_ID, clientSecret: process.env.DISCORD_SECRET }),
@@ -156,4 +160,4 @@ function AuthHandler(...args: any[]) {
export default AuthHandler(authConfig)
export const config = { runtime: "experimental-edge" }
export const config = { runtime: "edge" }

View File

@@ -1,9 +1,8 @@
// This is an example of to protect an API route
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "../auth/[...nextauth]"
import { getServerSession } from "auth"
export default async (req, res) => {
const session = await unstable_getServerSession(req, res, authOptions)
const session = await getServerSession(req, res)
if (session) {
res.send({

View File

@@ -1,8 +0,0 @@
// This is an example of how to access a session from an API route
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "../auth/[...nextauth]"
export default async (req, res) => {
const session = await unstable_getServerSession(req, res, authOptions)
res.json(session)
}

View File

@@ -0,0 +1,8 @@
import { authConfig } from "../auth-old/[...nextauth]"
// This is an example of how to access a session from an API route
import { getServerSession } from "next-auth/next"
export default async (req, res) => {
const session = await getServerSession(req, res, authConfig as any)
res.json(session)
}

View File

@@ -1,30 +0,0 @@
// This is an example of how to query data from Supabase with RLS.
// Learn more about Row Levele Security (RLS): https://supabase.com/docs/guides/auth/row-level-security
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "../auth/[...nextauth]"
import { createClient } from "@supabase/supabase-js"
export default async (req, res) => {
const session = await unstable_getServerSession(req, res, authOptions)
if (!session)
return res.send(JSON.stringify({ error: "No session!" }, null, 2))
const { supabaseAccessToken } = session
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
global: {
headers: {
Authorization: `Bearer ${supabaseAccessToken}`,
},
},
}
)
// Now you can query with RLS enabled.
const { data, error } = await supabase.from("users").select("*")
res.send(JSON.stringify({ supabaseAccessToken, data, error }, null, 2))
}

View File

@@ -1,67 +0,0 @@
// eslint-disable-next-line no-use-before-define
import * as React from "react"
import { signIn, signOut, useSession } from "next-auth/react"
import Layout from "components/layout"
export default function Page() {
const [response, setResponse] = React.useState(null)
const handleLogin = (options) => async () => {
if (options.redirect) {
return signIn("credentials", options)
}
const response = await signIn("credentials", options)
setResponse(response)
}
const handleLogout = (options) => async () => {
if (options.redirect) {
return signOut(options)
}
const response = await signOut(options)
setResponse(response)
}
const { data: session } = useSession()
if (session) {
return (
<Layout>
<h1>Test different flows for Credentials logout</h1>
<span className="spacing">Default:</span>
<button onClick={handleLogout({ redirect: true })}>Logout</button>
<br />
<span className="spacing">No redirect:</span>
<button onClick={handleLogout({ redirect: false })}>Logout</button>
<br />
<p>Response:</p>
<pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response, null, 2)}
</pre>
</Layout>
)
}
return (
<Layout>
<h1>Test different flows for Credentials login</h1>
<span className="spacing">Default:</span>
<button onClick={handleLogin({ redirect: true, password: "password" })}>
Login
</button>
<br />
<span className="spacing">No redirect:</span>
<button onClick={handleLogin({ redirect: false, password: "password" })}>
Login
</button>
<br />
<span className="spacing">No redirect, wrong password:</span>
<button onClick={handleLogin({ redirect: false, password: "" })}>
Login
</button>
<p>Response:</p>
<pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response, null, 2)}
</pre>
</Layout>
)
}

View File

@@ -0,0 +1,67 @@
import * as React from "react"
import { signIn, signOut, useSession } from "@auth/nextjs/react"
import { SignInResponse, SignOutResponse } from "@auth/nextjs/lib/client"
export default function Page() {
const [response, setResponse] = React.useState<
SignInResponse | SignOutResponse
>()
const { data: session } = useSession()
if (session) {
return (
<>
<h1>Test different flows for Credentials logout</h1>
<span className="spacing">Default: </span>
<button onClick={() => signOut()}>Logout</button>
<br />
<span className="spacing">No redirect: </span>
<button onClick={() => signOut({ redirect: false }).then(setResponse)}>
Logout
</button>
<br />
<p>{response ? "Response:" : "Session:"}</p>
<pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response ?? session, null, 2)}
</pre>
</>
)
}
return (
<>
<h1>Test different flows for Credentials login</h1>
<span className="spacing">Default: </span>
<button onClick={() => signIn("credentials", { password: "password" })}>
Login
</button>
<br />
<span className="spacing">No redirect: </span>
<button
onClick={() =>
signIn("credentials", { redirect: false, password: "password" }).then(
setResponse
)
}
>
Login
</button>
<br />
<span className="spacing">No redirect, wrong password: </span>
<button
onClick={() =>
signIn("credentials", { redirect: false, password: "wrong" }).then(
setResponse
)
}
>
Login
</button>
<p>Response:</p>
<pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response, null, 2)}
</pre>
</>
)
}

View File

@@ -1,9 +0,0 @@
import Layout from "components/layout"
export default function Page() {
return (
<Layout>
<h1>Page protected by Middleware</h1>
</Layout>
)
}

View File

@@ -1,8 +1,6 @@
import Layout from "../components/layout"
export default function Page() {
return (
<Layout>
<>
<p>
This is an example site to demonstrate how to use{" "}
<a href="https://authjs.dev">Auth.js</a> for authentication.
@@ -18,15 +16,11 @@ export default function Page() {
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</p>
<h2>Privacy Policy</h2>
<p>
This site uses JSON Web Tokens and an in-memory database which resets
every ~2 hours.
</p>
<p>
Data provided to this site is exclusively used to support signing in and
is not passed to any third party services, other than via SMTP or OAuth
for the purposes of authentication.
</p>
</Layout>
</>
)
}

View File

@@ -1,52 +1,34 @@
// This is an example of how to protect content using server rendering
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "./api/auth/[...nextauth]"
import Layout from "../components/layout"
import AccessDenied from "../components/access-denied"
import { getServerSession } from "auth"
import AccessDenied from "components/access-denied"
export default function Page({ content, session }) {
// If no session exists, display access denied message
if (!session) {
return (
<Layout>
<AccessDenied />
</Layout>
)
}
if (!session) return <AccessDenied />
// If session exists, display content
return (
<Layout>
<>
<h1>Protected Page</h1>
<p>
<strong>{content}</strong>
</p>
</Layout>
</>
)
}
export async function getServerSideProps(context) {
const session = await unstable_getServerSession(
context.req,
context.res,
authOptions
)
let content = null
const session = await getServerSession(context.req, context.res)
if (session) {
// Note usually you don't need to fetch from an API route in getServerSideProps
// This is done here to demonstrate how you can fetch from a third-party API
// with a valid session. Likely you would also not pass cookies but an `Authorization` header
const hostname = process.env.NEXTAUTH_URL || "http://localhost:3000"
const options = { headers: { cookie: context.req.headers.cookie } }
const res = await fetch(`${hostname}/api/examples/protected`, options)
const json = await res.json()
if (json.content) {
content = json.content
}
const res = await fetch(`${hostname}/api/examples/protected`, {
headers: { cookie: context.req.headers.cookie },
})
return { props: { session, content: (await res.json()).content } }
}
return {
props: {
session,
content,
},
}
return { props: {} }
}

View File

@@ -1,50 +0,0 @@
import { unstable_getServerSession } from "next-auth/next"
import Layout from "../components/layout"
import { authOptions } from "./api/auth/[...nextauth]"
export default function Page() {
// As this page uses Server Side Rendering, the `session` will be already
// populated on render without needing to go through a loading stage.
// This is possible because of the shared context configured in `_app.js` that
// is used by `useSession()`.
return (
<Layout>
<h1>Server Side Rendering</h1>
<p>
This page uses the <strong>unstable_getServerSession()</strong> method
in <strong>getServerSideProps()</strong>.
</p>
<p>
Using <strong>unstable_getServerSession()</strong> in{" "}
<strong>getServerSideProps()</strong> is currently the recommended
approach, although the API may still change, if you need to support
Server Side Rendering with authentication.
</p>
<p>
Using <strong>getSession()</strong> is still recommended on the client.
</p>
<p>
The advantage of Server Side Rendering is this page does not require
client side JavaScript.
</p>
<p>
The disadvantage of Server Side Rendering is that this page is slower to
render.
</p>
</Layout>
)
}
// Export the `session` prop to use sessions with Server Side Rendering
export async function getServerSideProps(context) {
return {
props: {
session: await unstable_getServerSession(
context.req,
context.res,
authOptions
),
},
}
}

View File

@@ -1,48 +0,0 @@
import Layout from "../components/layout"
import { useState, useEffect } from "react"
import { useSession } from "next-auth/react"
import { createClient } from "@supabase/supabase-js"
export default function Page() {
const { data: session } = useSession()
const [data, setData] = useState(null)
useEffect(() => {
if (session) {
// User is logged in, let's fetch their data.
const { supabaseAccessToken } = session
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
global: {
headers: { Authorization: `Bearer ${supabaseAccessToken}` },
},
}
)
// Fetch data with RLS enabled.
supabase
.from("users")
.select("*")
.then(({ data }) => setData(data))
}
}, [session])
return (
<Layout>
<h1>Fetch Data from Supabase with RLS</h1>
<h2>Client-side data fetching with RLS:</h2>
<pre>{JSON.stringify(data, null, 2)}</pre>
<h2>API Example</h2>
<p>
You can also use Supabase in API routes. See the code in the
`/pages/api/examples/supabase-rls.js` file.
</p>
<p>
<em>You must be signed in to see responses.</em>
</p>
<p>/api/examples/supabase-rls</p>
<iframe src="/api/examples/supabase-rls" />
</Layout>
)
}

View File

@@ -1,68 +0,0 @@
// This is an example of how to protect content using server rendering
// and fetching data from Supabase with RLS enabled.
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "./api/auth/[...nextauth]"
import { createClient } from "@supabase/supabase-js"
import Layout from "../components/layout"
import AccessDenied from "../components/access-denied"
export default function Page({ data, session }) {
// If no session exists, display access denied message
if (!session) {
return (
<Layout>
<AccessDenied />
</Layout>
)
}
// If session exists, display content
return (
<Layout>
<h1>Protected Page</h1>
<p>Data fetched during SSR from Supabase with RSL enabled:</p>
<pre>{JSON.stringify(data, null, 2)}</pre>
</Layout>
)
}
export async function getServerSideProps(context) {
const session = await unstable_getServerSession(
context.req,
context.res,
authOptions
)
if (!session)
return {
props: {
session,
data: null,
error: "No session",
},
}
const { supabaseAccessToken } = session
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
global: {
headers: {
Authorization: `Bearer ${supabaseAccessToken}`,
},
},
}
)
// Now you can query with RLS enabled.
const { data, error } = await supabase.from("users").select("*")
return {
props: {
session,
data,
error,
},
}
}

View File

@@ -18,3 +18,12 @@ declare module "next-auth" {
foo: string
}
}
declare global {
namespace NodeJS {
interface ProcessEnv {
[key: string]: string;
}
}
}

View File

@@ -1,19 +1,17 @@
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET= # Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32
AUTH_SECRET= # Linux: `openssl rand -hex 32` or go to https://generate-secret.vercel.app/32
AUTH_AUTH0_ID=
AUTH_AUTH0_SECRET=
AUTH_AUTH0_ISSUER=
AUTH0_ID=
AUTH0_SECRET=
AUTH0_ISSUER=
AUTH_FACEBOOK_ID=
AUTH_FACEBOOK_SECRET=
FACEBOOK_ID=
FACEBOOK_SECRET=
AUTH_GITHUB_ID=
AUTH_GITHUB_SECRET=
GITHUB_ID=
GITHUB_SECRET=
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
GOOGLE_ID=
GOOGLE_SECRET=
TWITTER_ID=
TWITTER_SECRET=
AUTH_TWITTER_ID=
AUTH_TWITTER_SECRET=

View File

@@ -8,7 +8,7 @@
<a href="https://nextjs.org" target="_blank">
<img height="64" src="https://nextjs.org/static/favicon/android-chrome-192x192.png" />
</a>
<h3 align="center"><b>NextAuth.js</b> - Example App</h3>
<h3 align="center"><b>next-auth</b> - Example App</h3>
<p align="center">
Open Source. Full Stack. Own Your Data.
</p>
@@ -36,7 +36,7 @@ This is an example application that shows how `next-auth` is applied to a basic
The deployed version can be found at [`next-auth-example.vercel.app`](https://next-auth-example.vercel.app)
Go to [next-auth.js.org](https://next-auth.js.org) for more information and documentation.
Go to [authjs.dev](https://authjs.dev) for more information and documentation.
## Getting Started
@@ -66,7 +66,7 @@ You **can** skip configuring a database and come back to it later if you want.
For more information about setting up a database, please check out the following links:
- Docs: [next-auth.js.org/adapters/overview](https://next-auth.js.org/adapters/overview)
- Docs: [authjs.dev/reference/adapters](https://authjs.dev/reference/adapters)
### 3. Configure Authentication Providers
@@ -76,7 +76,7 @@ For more information about setting up a database, please check out the following
e.g. For Google OAuth you would use: `http://localhost:3000/api/auth/callback/google`
A list of configured providers and their callback URLs is available from the endpoint `/api/auth/providers`. You can find more information at https://next-auth.js.org/configuration/providers/oauth
A list of configured providers and their callback URLs is available from the endpoint `/api/auth/providers`. You can find more information at [authjs.dev/getting-started/oauth-tutorial](https://authjs.dev/getting-started/oauth-tutorial)
3. You can also choose to specify an SMTP server for passwordless sign in via email.
@@ -106,4 +106,4 @@ Follow the [Deployment documentation](https://authjs.dev/guides/basics/deploymen
<a href="https://vercel.com?utm_source=nextauthjs&utm_campaign=oss">
<img width="170px" src="https://raw.githubusercontent.com/nextauthjs/next-auth/main/docs/static/img/powered-by-vercel.svg" alt="Powered By Vercel" />
</a>
<p align="left">Thanks to Vercel sponsoring this project by allowing it to be deployed for free for the entire Auth.js Team</p>
<p align="left">Thanks to Vercel sponsoring this project by allowing it to be deployed for free for the entire Auth.js Team</p>

View File

@@ -0,0 +1,3 @@
import { handlers } from "auth"
export const { GET, POST } = handlers
export const runtime = "edge"

View File

@@ -0,0 +1,10 @@
import { auth } from "auth"
import { NextResponse } from "next/server"
export const GET = auth(function GET(req) {
if (req.auth) {
return NextResponse.json(req.auth)
}
return NextResponse.json({ message: "Not authenticated" }, { status: 401 })
})

View File

@@ -1,15 +1,13 @@
import Layout from "../components/layout"
export default function ClientPage() {
export default function Page() {
return (
<Layout>
<>
<h1>Client Side Rendering</h1>
<p>
This page uses the <strong>useSession()</strong> React Hook in the{" "}
<strong>&lt;Header/&gt;</strong> component.
<strong>&lt;/Header&gt;</strong> component.
</p>
<p>
The <strong>useSession()</strong> React Hook is easy to use and allows
The <strong>useSession()</strong> React Hook easy to use and allows
pages to render very quickly.
</p>
<p>
@@ -22,6 +20,6 @@ export default function ClientPage() {
The disadvantage of <strong>useSession()</strong> is that it requires
client side JavaScript.
</p>
</Layout>
</>
)
}

View File

@@ -0,0 +1,24 @@
import Header from "components/header"
import Footer from "components/footer"
import './styles.css'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<head></head>
<body>
<Header />
<main>
{children}
</main>
<Footer />
</body>
</html>
)
}
export const runtime = "experimental-edge"

View File

@@ -1,13 +1,13 @@
import Layout from "components/layout"
export default function Page() {
return (
<Layout>
<>
<h1>NextAuth.js Example</h1>
<p>
This is an example site to demonstrate how to use{" "}
<a href="https://authjs.dev">NextAuth.js</a> for authentication.
</p>
</Layout>
</>
)
}
export const runtime = "experimental-edge"

View File

@@ -0,0 +1,39 @@
import { auth } from "auth"
import { cookies, headers } from "next/headers"
function SignIn({ id, children, className }: any) {
const $cookies = cookies()
const csrfToken = $cookies.get("next-auth.csrf-token")?.value.split("|")[0]
return (
<form action={`/api/auth/signin/${id}`} method="post">
<button className={className} type="submit">{children}</button>
<input type="hidden" name="csrfToken" value={csrfToken} />
</form>
)
}
function SignOut({ children }: any) {
const $cookies = cookies()
const csrfToken = $cookies.get("next-auth.csrf-token")?.value.split("|")[0]
return (
<form action="/api/auth/signout" method="post">
<button type="submit">{children}</button>
<input type="hidden" name="csrfToken" value={csrfToken} />
</form>
)
}
export default async function Page() {
const session = await auth(headers())
if (session) {
return (
<>
<pre>{JSON.stringify(session, null, 2)}</pre>
<SignOut>Sign out</SignOut>
</>
)
}
return <SignIn id="github">Sign in with github</SignIn>
}
export const runtime = "experimental-edge"

View File

@@ -0,0 +1,33 @@
body {
color: red;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
padding: 0 1rem 1rem 1rem;
max-width: 680px;
margin: 0 auto;
background: #fff;
color: var(--color-text);
}
li,
p {
line-height: 1.5rem;
}
a {
font-weight: 500;
}
hr {
border: 1px solid #ddd;
}
iframe {
background: #ccc;
border: 1px solid #ccc;
height: 10rem;
width: 100%;
border-radius: 0.5rem;
filter: invert(1);
}

View File

@@ -0,0 +1,25 @@
import NextAuth from "next-auth"
import Auth0 from "@auth/core/providers/github"
import Facebook from "@auth/core/providers/facebook"
import GitHub from "@auth/core/providers/github"
import Google from "@auth/core/providers/google"
import Twitter from "@auth/core/providers/twitter"
export const { handlers, auth } = NextAuth({
providers: [GitHub, Auth0, Facebook, Google, Twitter],
callbacks: {
async authorized({ request, auth }) {
// if (request.method === "POST") {
// const [, token] = request.headers.get("Authorization")?.split(" ")
// const valid = validateToken(token)
// // If the request has a valid auth token, it is authorized
// if (valid) return true
// return NextResponse.json("Invalid auth token", { status: 401 })
// }
// Logged in users are authorized, otherwise, will redirect to login
// You could also return a custom redirect instead of the sign-in page
return !!auth
},
},
})

View File

@@ -1,6 +1,6 @@
import Link from "next/link"
import styles from "./footer.module.css"
import packageJSON from "../package.json"
import packageJSON from "next-auth/package.json"
export default function Footer() {
return (
@@ -8,10 +8,10 @@ export default function Footer() {
<hr />
<ul className={styles.navItems}>
<li className={styles.navItem}>
<a href="https://next-auth.js.org">Documentation</a>
<a href="https://authjs.dev">Documentation</a>
</li>
<li className={styles.navItem}>
<a href="https://www.npmjs.com/package/next-auth">NPM</a>
<a href="https://www.npmjs.com/package/@auth/core">NPM</a>
</li>
<li className={styles.navItem}>
<a href="https://github.com/nextauthjs/next-auth-example">GitHub</a>
@@ -20,7 +20,7 @@ export default function Footer() {
<Link href="/policy">Policy</Link>
</li>
<li className={styles.navItem}>
<em>next-auth@{packageJSON.dependencies["next-auth"]}</em>
<em>{packageJSON.version}</em>
</li>
</ul>
</footer>

View File

@@ -1,5 +1,6 @@
/* Set min-height to avoid page reflow while session loading */
.signedInStatus {
position: relative;
display: block;
min-height: 4rem;
width: 100%;

View File

@@ -1,65 +1,63 @@
import Link from "next/link"
import { signIn, signOut, useSession } from "next-auth/react"
import { auth } from "auth"
import { cookies, headers } from "next/headers"
import styles from "./header.module.css"
// The approach used in this component shows how to build a sign in and sign out
function SignIn({ id, children, className }: any) {
const $cookies = cookies()
const csrfToken = $cookies.get("next-auth.csrf-token")?.value.split("|")[0]
return (
<form action={`/api/auth/signin/${id}`} method="post">
<button className={className} type="submit">{children}</button>
<input type="hidden" name="csrfToken" value={csrfToken} />
</form>
)
}
function SignOut({ children, className }: any) {
const $cookies = cookies()
const csrfToken = $cookies.get("next-auth.csrf-token")?.value.split("|")[0]
return (
<form action="/api/auth/signout" method="post">
<button className={className} type="submit">{children}</button>
<input type="hidden" name="csrfToken" value={csrfToken} />
</form>
)
}
// The approach used in this component shows how to built a sign in and sign out
// component that works on pages which support both client and server side
// rendering, and avoids any flash incorrect content on initial page load.
export default function Header() {
const { data: session, status } = useSession()
const loading = status === "loading"
export default async function Header() {
const session = await auth(headers())
return (
<header>
<noscript>
<style>{`.nojs-show { opacity: 1; top: 0; }`}</style>
<style>{".nojs-show { opacity: 1; top: 0; }"}</style>
</noscript>
<div className={styles.signedInStatus}>
<p
className={`nojs-show ${
!session && loading ? styles.loading : styles.loaded
}`}
>
<p className={`nojs-show ${styles.loaded}`}>
{!session && (
<>
<span className={styles.notSignedInText}>
You are not signed in
</span>
<a
href={`/api/auth/signin`}
className={styles.buttonPrimary}
onClick={(e) => {
e.preventDefault()
signIn()
}}
>
Sign in
</a>
<SignIn className={styles.buttonPrimary}>Sign In</SignIn>
</>
)}
{session?.user && (
{session && (
<>
{session.user.image && (
<span
style={{ backgroundImage: `url('${session.user.image}')` }}
className={styles.avatar}
/>
<img src={session.user.image} className={styles.avatar} />
)}
<span className={styles.signedInText}>
<small>Signed in as</small>
<br />
<strong>{session.user.email ?? session.user.name}</strong>
<strong>{session.user.email} </strong>
{session.user.name ? `(${session.user.name})` : null}
</span>
<a
href={`/api/auth/signout`}
className={styles.button}
onClick={(e) => {
e.preventDefault()
signOut()
}}
>
Sign out
</a>
<SignOut className={styles.button}>Sign Out</SignOut>
</>
)}
</p>
@@ -73,7 +71,7 @@ export default function Header() {
<Link href="/client">Client</Link>
</li>
<li className={styles.navItem}>
<Link href="/server">Server</Link>
<Link href="/server-component">Server</Link>
</li>
<li className={styles.navItem}>
<Link href="/protected">Protected</Link>
@@ -82,13 +80,12 @@ export default function Header() {
<Link href="/api-example">API</Link>
</li>
<li className={styles.navItem}>
<Link href="/admin">Admin</Link>
</li>
<li className={styles.navItem}>
<Link href="/me">Me</Link>
<Link href="/middleware-protected">Middleware protected</Link>
</li>
</ul>
</nav>
</header>
)
}
export const runtime = "experimental-edge"

View File

@@ -1,13 +0,0 @@
import Header from "./header"
import Footer from "./footer"
import type { ReactNode } from "react"
export default function Layout({ children }: { children: ReactNode }) {
return (
<>
<Header />
<main>{children}</main>
<Footer />
</>
)
}

View File

@@ -1,17 +1,10 @@
import { withAuth } from "next-auth/middleware"
// export { auth as default } from "auth"
import { auth } from "auth"
import { NextResponse } from "next/server"
// More on how NextAuth.js middleware works: https://next-auth.js.org/configuration/nextjs#middleware
export default withAuth({
callbacks: {
authorized({ req, token }) {
// `/admin` requires admin role
if (req.nextUrl.pathname === "/admin") {
return token?.userRole === "admin"
}
// `/me` only requires the user to be logged in
return !!token
},
},
export default auth((req) => {
if (req.auth) return NextResponse.json(req.auth)
return NextResponse.json("Not authorized", { status: 401 })
})
export const config = { matcher: ["/admin", "/me"] }
export const config = { matcher: ["/middleware-protected"] }

View File

@@ -1,10 +0,0 @@
import "next-auth/jwt"
// Read more at: https://next-auth.js.org/getting-started/typescript#module-augmentation
declare module "next-auth/jwt" {
interface JWT {
/** The user's role. */
userRole?: "admin"
}
}

View File

@@ -0,0 +1,9 @@
/** @type {import("next").NextConfig} */
module.exports = {
webpack(config) {
config.experiments = { ...config.experiments, topLevelAwait: true }
return config
},
experimental: { appDir: true },
typescript: { ignoreBuildErrors: true },
}

View File

@@ -1,5 +1,6 @@
{
"private": true,
"name": "nextjs-example-app",
"description": "An example project for NextAuth.js with Next.js",
"repository": "https://github.com/nextauthjs/next-auth-example.git",
"bugs": {
@@ -18,8 +19,9 @@
"Lluis Agusti <hi@llu.lu>"
],
"dependencies": {
"@auth/core": "workspace:*",
"next": "latest",
"next-auth": "latest",
"next-auth": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},

View File

@@ -1,18 +0,0 @@
import { SessionProvider } from "next-auth/react"
import "./styles.css"
import type { AppProps } from "next/app"
import type { Session } from "next-auth"
// Use of the <SessionProvider> is mandatory to allow components that call
// `useSession()` anywhere in your application to access the `session` object.
export default function App({
Component,
pageProps: { session, ...pageProps },
}: AppProps<{ session: Session }>) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
)
}

View File

@@ -1,17 +0,0 @@
import Layout from "../components/layout"
export default function Page() {
return (
<Layout>
<h1>This page is protected by Middleware</h1>
<p>Only admin users can see this page.</p>
<p>
To learn more about the NextAuth middleware see&nbsp;
<a href="https://docs-git-misc-docs-nextauthjs.vercel.app/configuration/nextjs#middleware">
the docs
</a>
.
</p>
</Layout>
)
}

View File

@@ -1,19 +0,0 @@
import Layout from "../components/layout"
export default function ApiExamplePage() {
return (
<Layout>
<h1>API Example</h1>
<p>The examples below show responses from the example API endpoints.</p>
<p>
<em>You must be signed in to see responses.</em>
</p>
<h2>Session</h2>
<p>/api/examples/session</p>
<iframe src="/api/examples/session" />
<h2>JSON Web Token</h2>
<p>/api/examples/jwt</p>
<iframe src="/api/examples/jwt" />
</Layout>
)
}

View File

@@ -1,44 +0,0 @@
import NextAuth, { NextAuthOptions } from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import FacebookProvider from "next-auth/providers/facebook"
import GithubProvider from "next-auth/providers/github"
import TwitterProvider from "next-auth/providers/twitter"
import Auth0Provider from "next-auth/providers/auth0"
// For more information on each option (and a full list of options) go to
// https://next-auth.js.org/configuration/options
export const authOptions: NextAuthOptions = {
// https://next-auth.js.org/configuration/providers/oauth
providers: [
Auth0Provider({
clientId: process.env.AUTH0_ID,
clientSecret: process.env.AUTH0_SECRET,
issuer: process.env.AUTH0_ISSUER,
}),
FacebookProvider({
clientId: process.env.FACEBOOK_ID,
clientSecret: process.env.FACEBOOK_SECRET,
}),
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
TwitterProvider({
clientId: process.env.TWITTER_ID,
clientSecret: process.env.TWITTER_SECRET,
version: "2.0",
}),
],
callbacks: {
async jwt({ token }) {
token.userRole = "admin"
return token
},
},
}
export default NextAuth(authOptions)

View File

@@ -1,14 +0,0 @@
// This is an example of how to read a JSON Web Token from an API route
import { getToken } from "next-auth/jwt"
import type { NextApiRequest, NextApiResponse } from "next"
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
// If you don't have the NEXTAUTH_SECRET environment variable set,
// you will have to pass your secret as `secret` to `getToken`
const token = await getToken({ req })
res.send(JSON.stringify(token, null, 2))
}

View File

@@ -1,23 +0,0 @@
// This is an example of to protect an API route
import { getServerSession } from "next-auth/next"
import { authOptions } from "../auth/[...nextauth]"
import type { NextApiRequest, NextApiResponse } from "next"
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const session = await getServerSession(req, res, authOptions)
if (session) {
return res.send({
content:
"This is protected content. You can access this content because you are signed in.",
})
}
res.send({
error: "You must be signed in to view the protected content on this page.",
})
}

View File

@@ -1,13 +0,0 @@
// This is an example of how to access a session from an API route
import { getServerSession } from "next-auth"
import { authOptions } from "../auth/[...nextauth]"
import type { NextApiRequest, NextApiResponse } from "next"
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const session = await getServerSession(req, res, authOptions)
res.send(JSON.stringify(session, null, 2))
}

View File

@@ -1,13 +0,0 @@
import Layout from "../components/layout"
export default function IndexPage() {
return (
<Layout>
<h1>NextAuth.js Example</h1>
<p>
This is an example site to demonstrate how to use{" "}
<a href="https://next-auth.js.org">NextAuth.js</a> for authentication.
</p>
</Layout>
)
}

View File

@@ -1,12 +0,0 @@
import { useSession } from "next-auth/react"
import Layout from "../components/layout"
export default function MePage() {
const { data } = useSession()
return (
<Layout>
<pre>{JSON.stringify(data, null, 2)}</pre>
</Layout>
)
}

View File

@@ -1,32 +0,0 @@
import Layout from "../components/layout"
export default function PolicyPage() {
return (
<Layout>
<p>
This is an example site to demonstrate how to use{" "}
<a href="https://next-auth.js.org">NextAuth.js</a> for authentication.
</p>
<h2>Terms of Service</h2>
<p>
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</p>
<h2>Privacy Policy</h2>
<p>
This site uses JSON Web Tokens and an in-memory database which resets
every ~2 hours.
</p>
<p>
Data provided to this site is exclusively used to support signing in and
is not passed to any third party services, other than via SMTP or OAuth
for the purposes of authentication.
</p>
</Layout>
)
}

View File

@@ -1,40 +0,0 @@
import { useState, useEffect } from "react"
import { useSession } from "next-auth/react"
import Layout from "../components/layout"
import AccessDenied from "../components/access-denied"
export default function ProtectedPage() {
const { data: session } = useSession()
const [content, setContent] = useState()
// Fetch content from protected route
useEffect(() => {
const fetchData = async () => {
const res = await fetch("/api/examples/protected")
const json = await res.json()
if (json.content) {
setContent(json.content)
}
}
fetchData()
}, [session])
// If no session exists, display access denied message
if (!session) {
return (
<Layout>
<AccessDenied />
</Layout>
)
}
// If session exists, display content
return (
<Layout>
<h1>Protected Page</h1>
<p>
<strong>{content ?? "\u00a0"}</strong>
</p>
</Layout>
)
}

View File

@@ -1,44 +0,0 @@
import { getServerSession } from "next-auth/next"
import { authOptions } from "./api/auth/[...nextauth]"
import Layout from "../components/layout"
import type { GetServerSidePropsContext } from "next"
import { useSession } from "next-auth/react"
export default function ServerSidePage() {
const { data: session } = useSession()
// As this page uses Server Side Rendering, the `session` will be already
// populated on render without needing to go through a loading stage.
return (
<Layout>
<h1>Server Side Rendering</h1>
<p>
This page uses the <strong>getServerSession()</strong> method in{" "}
<strong>getServerSideProps()</strong>.
</p>
<p>
Using <strong>getServerSession()</strong> in{" "}
<strong>getServerSideProps()</strong> is the recommended approach if you
need to support Server Side Rendering with authentication.
</p>
<p>
The advantage of Server Side Rendering is this page does not require
client side JavaScript.
</p>
<p>
The disadvantage of Server Side Rendering is that this page is slower to
render.
</p>
<pre>{JSON.stringify(session, null, 2)}</pre>
</Layout>
)
}
// Export the `session` prop to use sessions with Server Side Rendering
export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
props: {
session: await getServerSession(context.req, context.res, authOptions),
},
}
}

View File

@@ -1,10 +1,14 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"target": "esnext",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
@@ -12,15 +16,24 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"incremental": true,
"jsx": "preserve",
"incremental": true
"baseUrl": ".",
"plugins": [
{
"name": "next"
}
],
"strictNullChecks": true
},
"include": [
"process.d.ts",
"next-env.d.ts",
"next-auth.d.ts",
"**/*.ts",
"**/*.tsx"
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": ["node_modules"]
"exclude": [
"node_modules",
"jest.config.js"
]
}

View File

@@ -269,7 +269,7 @@ Ultimately if your request is not accepted or is not actively in development, yo
</summary>
<p>
Auth.js by default uses JSON Web Tokens for saving the user's session. However, if you use a [database adapter](/guides/adapters/using-a-database-adapter), the database will be used to persist the user's session. You can force the usage of JWT when using a database [through the configuration options](/reference/configuration/auth-config#session). Since v4 all our JWT tokens are now encrypted by default with A256GCM.
Auth.js by default uses JSON Web Tokens for saving the user's session. However, if you use a [database adapter](/guides/adapters/using-a-database-adapter), the database will be used to persist the user's session. You can force the usage of JWT when using a database [through the configuration options](/reference/configuration/auth-config#session). Since v4 all our JWTs are now encrypted by default with A256GCM.
</p>
</details>

View File

@@ -36,10 +36,6 @@ This tutorial assumes you have a Next.js application set up. If you don't, you c
npm install next-auth
```
:::info
We are working on a new `@auth/nextjs` package that will make it easier to set up Auth.js with Next.js. Stay tuned! For now, you can use the `next-auth` package.
:::
### Creating the server config
Create the following [API route](https://nextjs.org/docs/api-routes/dynamic-api-routes#catch-all-api-routes) file. This route contains the necessary configuration for NextAuth.js, as well as the dynamic route handler:
@@ -270,7 +266,7 @@ Note that, for each provider, the configuration process will be similar to what
2. Create create your OAuth application within it
3. Set the callback URL
4. Get the Client ID and Generate a Client Secret
:::
:::
## 3. Wiring all together

View File

@@ -0,0 +1,32 @@
---
title: Upgrade Guide (v5)
---
NextAuth.js version 5 will continue to be shipped as `next-auth` *for the Next.js version only*. We're here to help you upgrade your applications as smoothly as possible. It is possible to upgrade from any version of 4.x to the latest v5 release by following the migration steps below.
Upgrade to the latest version by running:
```bash npm2yarn2pnpm
npm install next-auth
```
## Getting Started
Below is a summary of the high-level API changes in `next-auth` v5.
```
| Where | Old | New |
| ------------------------- | --------------------------------------------------- | -------------- |
| API Route (Node) | getServerSession(req, res, authOptions) | auth() wrapper |
| API Route (Edge) | - | auth() wrapper |
| getServerSideProps | getServerSession(ctx.req, ctx.res, authOptions) | auth() wrapper |
| Middleware | withAuth(middleware, subset of authOptions) wrapper | auth() wrapper |
| Route Handler | - | auth() wrapper |
| Server Component | getServerSession(authOptions) | auth() call |
| Client Component | useSession() hook | useAuth() hook |
```
## Summary
We hope this migration goes smoothly for each and every one of you! If you have any questions or get stuck anywhere, feel free to create [a new issue](https://github.com/nextauthjs/next-auth/issues/new) on GitHub.

View File

@@ -29,7 +29,7 @@ Sent when the user signs out.
The message object will contain one of these depending on if you use JWT or database persisted sessions:
- `token`: The JWT token for this session.
- `token`: The JWT for this session.
- `session`: The session object from your adapter that is being ended
### createUser
@@ -60,5 +60,5 @@ Sent at the end of a request for the current session.
The message object will contain one of these depending on if you use JWT or database persisted sessions:
- `token`: The JWT token for this session.
- `token`: The JWT for this session.
- `session`: The session object from your adapter.

View File

@@ -2,7 +2,7 @@
title: Overview
---
Using a Auth.js / NextAuth.js adapter you can connect to any database service or even several different services at the same time. The following listed official adapters are created and maintained by the community:
Using an Auth.js / NextAuth.js adapter you can connect to any database service or even several different services at the same time. The following listed official adapters are created and maintained by the community:
<div class="adapter-card-list">
<a href="/reference/adapter/dgraph" class="adapter-card">
@@ -71,7 +71,7 @@ If you don't find an adapter for the database or service you use, you can always
## Models
Auth.js can be used with any database. Models tell you what structures Auth.js expects from your database. Models will vary slightly depending on which adapter you use, but in general, will look something like this. Each adapter's model/schema will be slightly adapted for its needs, but will look very much like this schema below:
Auth.js can be used with any database. Models tell you what structures Auth.js expects from your database. Models will vary slightly depending on which adapter you use, but in general, will look something like this:
```mermaid
erDiagram
@@ -96,15 +96,8 @@ erDiagram
string type
string provider
string providerAccountId
string refresh_token
string access_token
int expires_at
string token_type
string scope
string id_token
string session_state
string oauth_token_secret
string oauth_token
}
VerificationToken {
string identifier
@@ -113,10 +106,10 @@ erDiagram
}
```
More information about each Model / Table can be found below.
More information about each Model/Table can be found below.
:::note
You can [create your own adapter](/guides/adapters/creating-a-database-adapter) if you want to use Auth.js with a database that is not supported out of the box, or you have to change fields on any of the models.
You can [create your adapter](/guides/adapters/creating-a-database-adapter) if you want to use Auth.js with a database that is not supported out of the box, or you have to change fields on any of the models.
:::
---
@@ -125,30 +118,31 @@ You can [create your own adapter](/guides/adapters/creating-a-database-adapter)
The User model is for information such as the user's name and email address.
Email address is optional, but if one is specified for a User then it must be unique.
Email address is optional, but if one is specified for a User, then it must be unique.
:::note
If a user first signs in with OAuth then their email address is automatically populated using the one from their OAuth profile, if the OAuth provider returns one.
If a user first signs in with an OAuth provider, then their email address is automatically populated using the one from their OAuth profile if the OAuth provider returns one.
This provides a way to contact users and for users to maintain access to their account and sign in using email in the event they are unable to sign in with the OAuth provider in future (if the [Email Provider](/getting-started/email-tutorial) is configured).
This provides a way to contact users and for users to maintain access to their account and sign in using email in the event they are unable to sign in with the OAuth provider in the future (if the [Email Provider](/reference/core/providers_email) is configured).
:::
User creation in the database is automatic, and happens when the user is logging in for the first time with a provider. The default data saved is `id`, `name`, `email` and `image`. You can add more profile data by returning extra fields in your [OAuth provider](/guides/providers/custom-provider)'s [`profile()`](/reference/core/providers#profile) callback.
User creation in the database is automatic and happens when the user is logging in for the first time with a provider.
If the first sign-in is via the [OAuth Provider](/reference/core/providers_oauth), the default data saved is `id`, `name`, `email` and `image`. You can add more profile data by returning extra fields in your [OAuth provider](/guides/providers/custom-provider)'s [`profile()`](/reference/core/providers#profile) callback.
If the first sign-in is via the [Email Provider](/reference/core/providers_email), then the saved user will have `id`, `email`, `emailVerified`, where `emailVerified` is the timestamp of when the user was created.
### Account
The Account model is for information about OAuth accounts associated with a User. It will usually contain `access_token`, `id_token` and other OAuth specific data. [`TokenSet`](https://github.com/panva/node-openid-client/blob/main/docs/README.md#new-tokensetinput) from `openid-client` might give you an idea of all the fields.
:::note
In case of an OAuth 1.0 provider (like Twitter), you will have to look for `oauth_token` and `oauth_token_secret` string fields. GitHub also has an extra `refresh_token_expires_in` integer field. You have to make sure that your database schema includes these fields.
:::
The Account model is for information about OAuth accounts associated with a User
A single User can have multiple Accounts, but each Account can only have one User.
Linking Accounts to Users happen automatically, only when they have the same e-mail address, and the user is currently signed in. Check the [FAQ](/concepts/faq#security) for more information why this is a requirement.
Account creation in the database is automatic and happens when the user is logging in for the first time with a provider, or the [`Adapter.linkAccount`](/reference/core/adapters#linkaccount) method is invoked. The default data saved is `access_token`, `refresh_token`, `id_token` and `expires_at`. You can save other fields by returning them in the [OAuth provider](/guides/providers/custom-provider)'s [`account()`](/reference/core/providers#account) callback.
Linking Accounts to Users happen automatically, only when they have the same e-mail address, and the user is currently signed in. Check the [FAQ](/concepts/faq#security) for more information on why this is a requirement.
:::tip
You can manually unlink accounts, if your adapter implements the `unlinkAccount` method. Make sure to take all the necessary security steps to avoid data loss.
You can manually unlink accounts if your adapter implements the `unlinkAccount` method. Make sure to take all the necessary security steps to avoid data loss.
:::
:::note
@@ -162,7 +156,7 @@ The Session model is used for database sessions. It is not used if JSON Web Toke
A single User can have multiple Sessions, each Session can only have one User.
:::tip
When a Session is read, we check if it's `expires` field indicates an invalid session, and delete it from the database. You can also do this clean-up periodically in the background to avoid our extra delete call to the database during an active session retrieval. This might result in a slight performance increase in a few cases.
When a Session is read, we check if its `expires` field indicates an invalid session, and delete it from the database. You can also do this clean-up periodically in the background to avoid our extra delete call to the database during an active session retrieval. This might result in a slight performance increase in a few cases.
:::
### Verification Token
@@ -171,7 +165,7 @@ The Verification Token model is used to store tokens for passwordless sign in.
A single User can have multiple open Verification Tokens (e.g. to sign in to different devices).
It has been designed to be extendable for other verification purposes in the future (e.g. 2FA / short codes).
It has been designed to be extendable for other verification purposes in the future (e.g. 2FA / magic codes, etc.).
:::note
Auth.js makes sure that every token is usable only once, and by default has a short (1 day, can be configured by [`maxAge`](/guides/providers/email)) lifetime. If your user did not manage to finish the sign-in flow in time, they will have to start the sign-in process again.
@@ -183,8 +177,7 @@ Due to users forgetting or failing at the sign-in flow, you might end up with un
## RDBMS Naming Convention
Auth.js / NextAuth.js uses `camelCase` for its own database rows, while respecting the conventional `snake_case` formatting for OAuth related values. If mixed casing is an issue for you, most adapters have a dedicated section on how to use a single naming convention.
Auth.js / NextAuth.js uses `camelCase` for its database rows while respecting the conventional `snake_case` formatting for OAuth-related values. If the mixed casing is an issue for you, most adapters have a dedicated documentation section on how to force a casing convention.
## TypeScript

View File

@@ -12,9 +12,10 @@ The API reference is being migrated from the [old documentation page](https://ne
Here are the _currently_ planned and released packages under the `@auth/*` scope. This is not an exhaustive list, but the set of packages that we would like to focus on to begin with.
| Feature | Status |
| Package | Status |
| ------------------- | -------- |
| `@auth/nextjs` | Planned |
| `next-auth@4 ` | Stable. See [docs](https://next-auth.js.org/) |
| `@auth/nextjs` | Experimental, under `next-auth@5`. |
| `@auth/*-adapter` | Planned |
| `@auth/core` | Experimental |
| `@auth/sveltekit` | Experimental |

View File

@@ -1,7 +0,0 @@
---
title: Client
---
:::warning WIP
`@auth/nextjs/client` is work in progress. For now, please use [NextAuth.js Client API](https://next-auth.js.org/getting-started/client).
:::

View File

@@ -1,7 +0,0 @@
---
title: Next.js Auth
---
:::warning WIP
`@auth/nextjs` is work in progress. For now, please use [NextAuth.js](https://next-auth.js.org).
:::

View File

@@ -7,7 +7,7 @@ const path = require("path")
const coreSrc = "../packages/core/src"
const providers = fs
.readdirSync(path.join(__dirname, coreSrc, "/providers"))
.filter((file) => file.endsWith(".ts") && !file.startsWith("oauth"))
.filter((file) => file.endsWith(".ts"))
.map((p) => `${coreSrc}/providers/${p}`)
const typedocConfig = require("./typedoc.json")
@@ -38,6 +38,22 @@ function typedocAdapter(name) {
]
}
function typedocFramework(id, entrypoints) {
return [
"docusaurus-plugin-typedoc",
{
...typedocConfig,
id: id.replace("frameworks-", ""),
plugin: [require.resolve("./typedoc-mdn-links")],
watch: process.env.TYPEDOC_WATCH,
entryPoints: entrypoints.map((e) => `../packages/${id}/src/${e}`),
tsconfig: `../packages/${id}/tsconfig.json`,
out: `reference/${id.replace("frameworks-", "")}`,
sidebar: { indexLabel: "index" },
},
]
}
/** @type {import("@docusaurus/types").Config} */
const docusaurusConfig = {
markdown: {
@@ -231,36 +247,9 @@ const docusaurusConfig = {
],
],
plugins: [
[
"docusaurus-plugin-typedoc",
{
...typedocConfig,
id: "core",
plugin: [require.resolve("./typedoc-mdn-links")],
watch: process.env.TYPEDOC_WATCH,
entryPoints: ["index.ts", "adapters.ts", "errors.ts", "jwt.ts", "types.ts"].map((e) => `${coreSrc}/${e}`).concat(providers),
tsconfig: "../packages/core/tsconfig.json",
out: "reference/core",
sidebar: {
indexLabel: "index",
},
},
],
[
"docusaurus-plugin-typedoc",
{
...typedocConfig,
id: "sveltekit",
plugin: [require.resolve("./typedoc-mdn-links")],
watch: process.env.TYPEDOC_WATCH,
entryPoints: ["index.ts", "client.ts"].map((e) => `../packages/frameworks-sveltekit/src/lib/${e}`),
tsconfig: "../packages/frameworks-sveltekit/tsconfig.json",
out: "reference/sveltekit",
sidebar: {
indexLabel: "index",
},
},
],
typedocFramework("core", ["index.ts", "adapters.ts", "errors.ts", "jwt.ts", "types.ts"]),
typedocFramework("frameworks-sveltekit", ["lib/index.ts", "lib/client.ts"]),
typedocFramework("frameworks-nextjs", ["index.ts", "react.tsx", "jwt.ts", "adapters.ts", "next.ts", "types.ts", "middleware.ts"]),
...(process.env.TYPEDOC_SKIP_ADAPTERS
? []
: [

View File

@@ -35,16 +35,9 @@ module.exports = {
},
{
type: "category",
label: "@auth/nextjs",
label: "next-auth",
link: { type: "doc", id: "reference/nextjs/index" },
items: [
"reference/nextjs/client",
{
type: "link",
label: "NextAuth.js (next-auth)",
href: "https://next-auth.js.org",
},
],
items: [{ type: "autogenerated", dirName: "reference/nextjs" }],
},
...(process.env.TYPEDOC_SKIP_ADAPTERS
? []

View File

@@ -66,6 +66,16 @@
"has": [{ "type": "host", "value": "solid-start.authjs.dev" }],
"destination": "https://authjs.dev/reference/solid-start"
},
{
"source": "/:path(.*)",
"has": [{ "type": "host", "value": "nextjs.authjs.dev" }],
"destination": "https://authjs.dev/reference/nextjs"
},
{
"source": "/v5",
"has": [{ "type": "host", "value": "nextjs.authjs.dev" }],
"destination": "https://authjs.dev/getting-started/upgrade-to-v5"
},
{
"source": "/:path(.*)",
"has": [{ "type": "host", "value": "errors.authjs.dev" }],

View File

@@ -9,6 +9,7 @@
"build": "turbo run build --filter=next-auth --filter=@next-auth/* --filter=@auth/* --no-deps",
"test": "turbo run test --concurrency=1 --filter=[HEAD^1] --filter=./packages/* --filter=!@*upstash* --filter=!*dynamodb-*",
"clean": "turbo run clean --no-cache",
"dev:example": "turbo run dev --parallel --continue --filter=nextjs-example-app... --filter=!./packages/adapter-*",
"dev:db": "turbo run dev --parallel --continue --filter=next-auth-app...",
"dev": "turbo run dev --parallel --continue --filter=next-auth-app... --filter=!./packages/adapter-*",
"dev-v4:db": "turbo run dev --parallel --continue --filter=next-auth-app-v4...",
@@ -28,7 +29,7 @@
"@actions/core": "^1.10.0",
"@balazsorban/monorepo-release": "0.1.8",
"@types/jest": "^28.1.3",
"@types/node": "^17.0.25",
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "5.47.0",
"@typescript-eslint/parser": "5.47.0",
"eslint": "8.30.0",
@@ -96,8 +97,6 @@
"packages/core/src/lib/pages/styles.ts",
"packages/frameworks-sveltekit/package",
"packages/frameworks-sveltekit/vite.config.{js,ts}.timestamp-*",
"packages/next-auth/src/providers/oauth-types.ts",
"packages/next-auth/css/index.css",
".branches",
"db.sqlite",
"dev.db",
@@ -248,8 +247,10 @@
"overrides": [
{
"files": [
"apps/dev/nextjs/pages/api/auth/[...nextauth].ts",
"docs/{sidebars,docusaurus.config}.js"
"apps/dev/nextjs/pages/api/auth-old/[...nextauth].ts",
"apps/dev/nextjs/app/api/auth/[...nextauth]/route.ts",
"docs/{sidebars,docusaurus.config}.js",
"packages/next-auth/src/lib/env.ts"
],
"options": {
"printWidth": 150

View File

@@ -42,8 +42,6 @@ import type { Adapter, AdapterAccount } from "next-auth/adapters"
* })
* ```
*
* ## Advanced usage
*
* ### Create the Prisma schema from scratch
*
* You need to use at least Prisma 2.26.0. Create a schema file in `prisma/schema.prisma` similar to this one:

View File

@@ -228,6 +228,10 @@ export interface Adapter {
deleteUser?(
userId: string
): Promise<void> | Awaitable<AdapterUser | null | undefined>
/**
* This method is invoked internally (but optionally can be used for manual linking).
* It creates an [Account](https://authjs.dev/reference/adapters#models) in the database.
*/
linkAccount?(
account: AdapterAccount
): Promise<void> | Awaitable<AdapterAccount | null | undefined>

View File

@@ -20,13 +20,6 @@ export class AuthError extends Error {
}
}
/**
* @todo
* Thrown when an Email address is already associated with an account
* but the user is trying an OAuth account that is not linked to it.
*/
export class AccountNotLinked extends AuthError {}
/**
* @todo
* One of the database `Adapter` methods failed.
@@ -37,8 +30,8 @@ export class AdapterError extends AuthError {}
export class AuthorizedCallbackError extends AuthError {}
/**
* There was an error while trying to finish up authenticating the user.
* Depending on the type of provider, this could be for multiple reasons.
* This error occurs when the user cannot finish the sign-in process.
* Depending on the provider type, this could have happened for multiple reasons.
*
* :::tip
* Check out `[auth][details]` in the error message to know which provider failed.
@@ -48,7 +41,7 @@ export class AuthorizedCallbackError extends AuthError {}
* ```
* :::
*
* For an **OAuth provider**, possible causes are:
* For an [OAuth provider](https://authjs.dev/reference/core/providers_oauth), possible causes are:
* - The user denied access to the application
* - There was an error parsing the OAuth Profile:
* Check out the provider's `profile` or `userinfo.request` method to make sure
@@ -56,7 +49,7 @@ export class AuthorizedCallbackError extends AuthError {}
* - The `signIn` or `jwt` callback methods threw an uncaught error:
* Check the callback method implementations.
*
* For an **Email provider**, possible causes are:
* For an [Email provider](https://authjs.dev/reference/core/providers_email), possible causes are:
* - The provided email/token combination was invalid/missing:
* Check if the provider's `sendVerificationRequest` method correctly sends the email.
* - The provided email/token combination has expired:
@@ -64,7 +57,7 @@ export class AuthorizedCallbackError extends AuthError {}
* - There was an error with the database:
* Check the database logs.
*
* For a **Credentials provider**, possible causes are:
* For a [Credentials provider](https://authjs.dev/reference/core/providers_credentials), possible causes are:
* - The `authorize` method threw an uncaught error:
* Check the provider's `authorize` method.
* - The `signIn` or `jwt` callback methods threw an uncaught error:
@@ -107,31 +100,87 @@ export class MissingAPIRoute extends AuthError {}
/** @todo */
export class MissingAuthorize extends AuthError {}
/** @todo */
/**
* Auth.js requires a secret to be set, but none was not found. This is used to encrypt cookies, JWTs and other sensitive data.
*
* :::note
* If you are using a framework like Next.js, we try to automatically infer the secret from the `AUTH_SECRET` environment variable.
* Alternatively, you can also explicitly set the [`AuthConfig.secret`](https://authjs.dev/reference/core#secret).
* :::
*
*
* :::tip
* You can generate a good secret value:
* - On Unix systems: type `openssl rand -hex 32` in the terminal
* - Or generate one [online](https://generate-secret.vercel.app/32)
*
* :::
*/
export class MissingSecret extends AuthError {}
/** @todo */
export class OAuthSignInError extends AuthError {}
/**
* @todo
* Thrown when an Email address is already associated with an account
* but the user is trying an OAuth account that is not linked to it.
*/
export class OAuthAccountNotLinked extends AuthError {}
/** @todo */
/**
* Thrown when an OAuth provider returns an error during the sign in process.
* This could happen for example if the user denied access to the application or there was a configuration error.
*
* For a full list of possible reasons, check out the specification [Authorization Code Grant: Error Response](https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2.1)
*/
export class OAuthCallbackError extends AuthError {}
/** @todo */
export class OAuthCreateUserError extends AuthError {}
/** @todo */
/**
* This error occurs during an OAuth sign in attempt when the provdier's
* response could not be parsed. This could for example happen if the provider's API
* changed, or the [`OAuth2Config.profile`](https://authjs.dev/reference/core/providers_oauth#profile) method is not implemented correctly.
*/
export class OAuthProfileParseError extends AuthError {}
/** @todo */
export class SessionTokenError extends AuthError {}
/** @todo */
/**
* This error occurs when the user cannot initiate the sign-in process.
* Depending on the provider type, this could have happened for multiple reasons.
*
* :::tip
* Check out `[auth][details]` in the error message to know which provider failed.
* @example
* ```sh
* [auth][details]: { "provider": "github" }
* ```
* :::
*
* For an [OAuth provider](https://authjs.dev/reference/core/providers_oauth), possible causes are:
* - The Authorization Server is not compliant with the [OAuth 2.0 specifcation](https://www.ietf.org/rfc/rfc6749.html)
* Check the details in the error message.
* - A runtime error occurred in Auth.js. This should be reported as a bug.
*
* For an [Email provider](https://authjs.dev/reference/core/providers_email), possible causes are:
* - The email sent from the client is invalid, could not be normalized by [`EmailConfig.normalizeIdentifier`](https://authjs.dev/reference/core/providers_email#normalizeidentifier)
* - The provided email/token combination has expired:
* Ask the user to log in again.
* - There was an error with the database:
* Check the database logs.
*
*/
export class SignInError extends AuthError {}
/** @todo */
export class SignOutError extends AuthError {}
/** @todo */
/**
* Auth.js was requested to handle an operation that it does not support.
*
* See [`AuthAction`](https://authjs.dev/reference/core/types#authaction) for the supported actions.
*/
export class UnknownAction extends AuthError {}
/** @todo */

View File

@@ -190,7 +190,7 @@ export interface JWTEncodeParams<Payload = JWT> {
/**
* The maximum age of the Auth.js issued JWT in seconds.
*
* @default 30 * 24 * 30 * 60 // 30 days
* @default 30 * 24 * 60 * 60 // 30 days
*/
maxAge?: number
}
@@ -213,7 +213,7 @@ export interface JWTOptions {
/**
* The maximum age of the Auth.js issued JWT in seconds.
*
* @default 30 * 24 * 30 * 60 // 30 days
* @default 30 * 24 * 60 * 60 // 30 days
*/
maxAge: number
/** Override this method to control the Auth.js issued JWT encoding. */

View File

@@ -1,4 +1,4 @@
import { AccountNotLinked } from "../errors.js"
import { OAuthAccountNotLinked } from "../errors.js"
import { fromDate } from "./utils/date.js"
import type {
@@ -49,7 +49,7 @@ export async function handleLogin(
}
const profile = _profile as AdapterUser
const account = _account as AdapterAccount
let account = _account as AdapterAccount
const {
createUser,
@@ -122,113 +122,116 @@ export async function handleLogin(
})
return { session, user, isNewUser }
} else if (account.type === "oauth" || account.type === "oidc") {
// If signing in with OAuth account, check to see if the account exists already
const userByAccount = await getUserByAccount({
providerAccountId: account.providerAccountId,
provider: account.provider,
})
if (userByAccount) {
if (user) {
// If the user is already signed in with this account, we don't need to do anything
if (userByAccount.id === user.id) {
return { session, user, isNewUser }
}
// If the user is currently signed in, but the new account they are signing in
// with is already associated with another user, then we cannot link them
// and need to return an error.
throw new AccountNotLinked(
"The account is already associated with another user",
{ provider: account.provider }
)
}
// If there is no active session, but the account being signed in with is already
// associated with a valid user then create session to sign the user in.
session = useJwtSession
? {}
: await createSession({
sessionToken: generateSessionToken(),
userId: userByAccount.id,
expires: fromDate(options.session.maxAge),
})
}
return { session, user: userByAccount, isNewUser }
} else {
if (user) {
// If the user is already signed in and the OAuth account isn't already associated
// with another user account then we can go ahead and link the accounts safely.
await linkAccount({ ...account, userId: user.id })
await events.linkAccount?.({ user, account, profile })
// As they are already signed in, we don't need to do anything after linking them
// If signing in with OAuth account, check to see if the account exists already
const userByAccount = await getUserByAccount({
providerAccountId: account.providerAccountId,
provider: account.provider,
})
if (userByAccount) {
if (user) {
// If the user is already signed in with this account, we don't need to do anything
if (userByAccount.id === user.id) {
return { session, user, isNewUser }
}
// If the user is currently signed in, but the new account they are signing in
// with is already associated with another user, then we cannot link them
// and need to return an error.
throw new OAuthAccountNotLinked(
"The account is already associated with another user",
{ provider: account.provider }
)
}
// If there is no active session, but the account being signed in with is already
// associated with a valid user then create session to sign the user in.
session = useJwtSession
? {}
: await createSession({
sessionToken: generateSessionToken(),
userId: userByAccount.id,
expires: fromDate(options.session.maxAge),
})
// If the user is not signed in and it looks like a new OAuth account then we
// check there also isn't an user account already associated with the same
// email address as the one in the OAuth profile.
//
// This step is often overlooked in OAuth implementations, but covers the following cases:
//
// 1. It makes it harder for someone to accidentally create two accounts.
// e.g. by signin in with email, then again with an oauth account connected to the same email.
// 2. It makes it harder to hijack a user account using a 3rd party OAuth account.
// e.g. by creating an oauth account then changing the email address associated with it.
//
// It's quite common for services to automatically link accounts in this case, but it's
// better practice to require the user to sign in *then* link accounts to be sure
// someone is not exploiting a problem with a third party OAuth service.
//
// OAuth providers should require email address verification to prevent this, but in
// practice that is not always the case; this helps protect against that.
const userByEmail = profile.email
? await getUserByEmail(profile.email)
: null
if (userByEmail) {
const provider = options.provider as OAuthConfig<any>
if (provider?.allowDangerousEmailAccountLinking) {
// If you trust the oauth provider to correctly verify email addresses, you can opt-in to
// account linking even when the user is not signed-in.
user = userByEmail
} else {
// We end up here when we don't have an account with the same [provider].id *BUT*
// we do already have an account with the same email address as the one in the
// OAuth profile the user has just tried to sign in with.
//
// We don't want to have two accounts with the same email address, and we don't
// want to link them in case it's not safe to do so, so instead we prompt the user
// to sign in via email to verify their identity and then link the accounts.
throw new AccountNotLinked(
"Another account already exists with the same e-mail address",
{ provider: account.provider }
)
}
} else {
// If the current user is not logged in and the profile isn't linked to any user
// accounts (by email or provider account id)...
//
// If no account matching the same [provider].id or .email exists, we can
// create a new account for the user, link it to the OAuth account and
// create a new session for them so they are signed in with it.
const { id: _, ...newUser } = { ...profile, emailVerified: null }
user = await createUser(newUser)
}
await events.createUser?.({ user })
return { session, user: userByAccount, isNewUser }
} else {
const { provider: p } = options as InternalOptions<"oauth" | "oidc">
const { type, provider, providerAccountId, userId, ...tokenSet } = account
const defaults = { providerAccountId, provider, type, userId }
account = Object.assign(p.account(tokenSet), defaults)
if (user) {
// If the user is already signed in and the OAuth account isn't already associated
// with another user account then we can go ahead and link the accounts safely.
await linkAccount({ ...account, userId: user.id })
await events.linkAccount?.({ user, account, profile })
session = useJwtSession
? {}
: await createSession({
sessionToken: generateSessionToken(),
userId: user.id,
expires: fromDate(options.session.maxAge),
})
return { session, user, isNewUser: true }
// As they are already signed in, we don't need to do anything after linking them
return { session, user, isNewUser }
}
}
throw new Error("Unsupported account type")
// If the user is not signed in and it looks like a new OAuth account then we
// check there also isn't an user account already associated with the same
// email address as the one in the OAuth profile.
//
// This step is often overlooked in OAuth implementations, but covers the following cases:
//
// 1. It makes it harder for someone to accidentally create two accounts.
// e.g. by signin in with email, then again with an oauth account connected to the same email.
// 2. It makes it harder to hijack a user account using a 3rd party OAuth account.
// e.g. by creating an oauth account then changing the email address associated with it.
//
// It's quite common for services to automatically link accounts in this case, but it's
// better practice to require the user to sign in *then* link accounts to be sure
// someone is not exploiting a problem with a third party OAuth service.
//
// OAuth providers should require email address verification to prevent this, but in
// practice that is not always the case; this helps protect against that.
const userByEmail = profile.email
? await getUserByEmail(profile.email)
: null
if (userByEmail) {
const provider = options.provider as OAuthConfig<any>
if (provider?.allowDangerousEmailAccountLinking) {
// If you trust the oauth provider to correctly verify email addresses, you can opt-in to
// account linking even when the user is not signed-in.
user = userByEmail
} else {
// We end up here when we don't have an account with the same [provider].id *BUT*
// we do already have an account with the same email address as the one in the
// OAuth profile the user has just tried to sign in with.
//
// We don't want to have two accounts with the same email address, and we don't
// want to link them in case it's not safe to do so, so instead we prompt the user
// to sign in via email to verify their identity and then link the accounts.
throw new OAuthAccountNotLinked(
"Another account already exists with the same e-mail address",
{ provider: account.provider }
)
}
} else {
// If the current user is not logged in and the profile isn't linked to any user
// accounts (by email or provider account id)...
//
// If no account matching the same [provider].id or .email exists, we can
// create a new account for the user, link it to the OAuth account and
// create a new session for them so they are signed in with it.
const { id: _, ...newUser } = { ...profile, emailVerified: null }
user = await createUser(newUser)
}
await events.createUser?.({ user })
await linkAccount({ ...account, userId: user.id })
await events.linkAccount?.({ user, account, profile })
session = useJwtSession
? {}
: await createSession({
sessionToken: generateSessionToken(),
userId: user.id,
expires: fromDate(options.session.maxAge),
})
return { session, user, isNewUser: true }
}
}

View File

@@ -110,14 +110,10 @@ export async function AuthInternal<
if (
[
"Signin",
"OAuthSignin",
"OAuthCallback",
"OAuthCreateAccount",
"EmailCreateAccount",
"Callback",
"OAuthAccountNotLinked",
"EmailSignin",
"CredentialsSignin",
"SessionRequired",
].includes(error as string)
) {

View File

@@ -3,6 +3,7 @@ import * as o from "oauth4webapi"
import { OAuthCallbackError, OAuthProfileParseError } from "../../errors.js"
import type {
Account,
InternalOptions,
LoggerInstance,
Profile,
@@ -88,11 +89,9 @@ export async function handleOAuth(
/** https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2.1 */
if (o.isOAuth2Error(codeGrantParams)) {
logger.debug("OAuthCallbackError", {
providerId: provider.id,
...codeGrantParams,
})
throw new OAuthCallbackError(codeGrantParams.error)
const cause = { providerId: provider.id, ...codeGrantParams }
logger.debug("OAuthCallbackError", cause)
throw new OAuthCallbackError("OAuth Provider returned an error", cause)
}
const codeVerifier = await checks.pkce.use(cookies, resCookies, options)
@@ -123,8 +122,8 @@ export async function handleOAuth(
throw new Error("TODO: Handle www-authenticate challenges as needed")
}
let profile: Profile = {}
let tokens: TokenSet
let profile: Profile
let tokens: TokenSet & Pick<Account, "expires_at">
if (provider.type === "oidc") {
const nonce = await checks.nonce.use(cookies, resCookies, options)
@@ -162,37 +161,49 @@ export async function handleOAuth(
(tokens as any).access_token
)
profile = await userinfoResponse.json()
} else {
throw new TypeError("No userinfo endpoint configured")
}
}
const profileResult = await getProfile(profile, provider, tokens, logger)
if (tokens.expires_in) {
tokens.expires_at =
Math.floor(Date.now() / 1000) + Number(tokens.expires_in)
}
const profileResult = await getUserAndProfile(
profile,
provider,
tokens,
logger
)
return { ...profileResult, cookies: resCookies }
}
/** Returns profile, raw profile and auth provider details */
async function getProfile(
async function getUserAndProfile(
OAuthProfile: Profile,
provider: OAuthConfigInternal<any>,
tokens: TokenSet,
logger: LoggerInstance
) {
try {
const profile = await provider.profile(OAuthProfile, tokens)
profile.email = profile.email?.toLowerCase()
const user = await provider.profile(OAuthProfile, tokens)
user.email = user.email?.toLowerCase()
if (!profile.id) {
if (!user.id) {
throw new TypeError(
`Profile id is missing in ${provider.name} OAuth profile response`
`User id is missing in ${provider.name} OAuth profile response`
)
}
return {
profile,
user,
account: {
provider: provider.id,
type: provider.type,
providerAccountId: profile.id.toString(),
providerAccountId: user.id.toString(),
...tokens,
},
OAuthProfile,
@@ -206,6 +217,8 @@ async function getProfile(
// redirected back to the sign up page. We log the error to help developers
// who might be trying to debug this when configuring a new provider.
logger.debug("getProfile error details", OAuthProfile)
logger.error(new OAuthProfileParseError(e as Error))
logger.error(
new OAuthProfileParseError(e as Error, { provider: provider.id })
)
}
}

View File

@@ -11,7 +11,7 @@ const signinErrors: Record<
default: "Unable to sign in.",
signin: "Try signing in with a different account.",
oauthsignin: "Try signing in with a different account.",
oauthcallback: "Try signing in with a different account.",
oauthcallbackerror: "Try signing in with a different account.",
oauthcreateaccount: "Try signing in with a different account.",
emailcreateaccount: "Try signing in with a different account.",
callback: "Try signing in with a different account.",

View File

@@ -1,13 +1,16 @@
import { OAuthProfileParseError } from "../errors.js"
import { merge } from "./utils/merge.js"
import type {
AccountCallback,
OAuthConfig,
OAuthConfigInternal,
OAuthEndpointType,
OAuthUserConfig,
ProfileCallback,
Provider,
} from "../providers/index.js"
import type { AuthConfig, InternalProvider } from "../types.js"
import type { AuthConfig, InternalProvider, Profile } from "../types.js"
/**
* Adds `signinUrl` and `callbackUrl` to each provider
@@ -77,18 +80,49 @@ function normalizeOAuth(
checks,
userinfo,
profile: c.profile ?? defaultProfile,
account: c.account ?? defaultAccount,
}
}
function defaultProfile(profile: any) {
return {
id: profile.sub ?? profile.id,
name:
profile.name ?? profile.nickname ?? profile.preferred_username ?? null,
email: profile.email ?? null,
image: profile.picture ?? null,
}
/**
* Returns basic user profile from the userinfo response/`id_token` claims.
* @see https://authjs.dev/reference/adapters#user
* @see https://openid.net/specs/openid-connect-core-1_0.html#IDToken
* @see https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
*/
const defaultProfile: ProfileCallback<Profile> = (profile) => {
const id = profile.sub ?? profile.id
if (!id) throw new OAuthProfileParseError("Missing user id")
return stripUndefined({
id: id.toString(),
name: profile.name ?? profile.nickname ?? profile.preferred_username,
email: profile.email,
image: profile.picture,
})
}
/**
* Returns basic OAuth/OIDC values from the token response.
* @see https://www.ietf.org/rfc/rfc6749.html#section-5.1
* @see https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
* @see https://authjs.dev/reference/adapters#account
*
* @todo Return `refresh_token` and `expires_at` as well when built-in
* refresh token support is added. (Can make it opt-in first with a flag).
*/
const defaultAccount: AccountCallback = (account) => {
return stripUndefined({
access_token: account.access_token,
id_token: account.id_token,
})
}
function stripUndefined<T extends object>(o: T): T {
const result = {} as any
for (let [k, v] of Object.entries(o)) v !== undefined && (result[k] = v)
return result as T
}
function normalizeEndpoint(
e?: OAuthConfig<any>[OAuthEndpointType],
issuer?: string

View File

@@ -1,4 +1,8 @@
import { CallbackRouteError, Verification } from "../../errors.js"
import {
CallbackRouteError,
OAuthCallbackError,
Verification,
} from "../../errors.js"
import { handleLogin } from "../callback-handler.js"
import { handleOAuth } from "../oauth/callback.js"
import { handleState } from "../oauth/handle-state.js"
@@ -68,14 +72,18 @@ export async function callback(params: {
logger.debug("authorization result", authorizationResult)
const { profile, account, OAuthProfile } = authorizationResult
const {
user: userFromProvider,
account,
OAuthProfile,
} = authorizationResult
// If we don't have a profile object then either something went wrong
// or the user cancelled signing in. We don't know which, so we just
// direct the user to the signin page for now. We could do something
// else in future.
// TODO: Handle user cancelling signin
if (!profile || !account || !OAuthProfile) {
if (!userFromProvider || !account || !OAuthProfile) {
return { redirect: `${url}/signin`, cookies }
}
@@ -83,7 +91,7 @@ export async function callback(params: {
// Attempt to get Profile from OAuth provider details before invoking
// signIn callback - but if no user object is returned, that is fine
// (that just means it's a new user signing in for the first time).
let userOrProfile = profile
let userByAccountOrFromProvider
if (adapter) {
const { getUserByAccount } = adapter
const userByAccount = await getUserByAccount({
@@ -91,11 +99,15 @@ export async function callback(params: {
provider: provider.id,
})
if (userByAccount) userOrProfile = userByAccount
if (userByAccount) userByAccountOrFromProvider = userByAccount
}
const unauthorizedOrError = await handleAuthorized(
{ user: userOrProfile, account, profile: OAuthProfile },
{
user: userByAccountOrFromProvider,
account,
profile: OAuthProfile,
},
options
)
@@ -104,7 +116,7 @@ export async function callback(params: {
// Sign user in
const { user, session, isNewUser } = await handleLogin(
sessionStore.value,
profile,
userFromProvider,
account,
options
)
@@ -152,7 +164,7 @@ export async function callback(params: {
})
}
await events.signIn?.({ user, account, profile, isNewUser })
await events.signIn?.({ user, account, profile: OAuthProfile, isNewUser })
// Handle first logins on new accounts
// e.g. option to send users to a new account landing page on initial login
@@ -360,8 +372,18 @@ export async function callback(params: {
cookies,
}
} catch (e) {
if (e instanceof OAuthCallbackError) {
logger.error(e)
// REVIEW: Should we expose original error= and error_description=
// Should we use a different name for error= then, since we already use it for all kind of errors?
url.searchParams.set("error", OAuthCallbackError.name)
url.pathname += "/signin"
return { redirect: url.toString(), cookies }
}
const error = new CallbackRouteError(e as Error, { provider: provider.id })
logger.debug("callback route error details", { method, query, body })
logger.error(error)
url.searchParams.set("error", CallbackRouteError.name)
url.pathname += "/error"

View File

@@ -41,7 +41,7 @@ export async function session(
user: {
name: decodedToken?.name,
email: decodedToken?.email,
image: decodedToken?.picture,
picture: decodedToken?.picture,
},
expires: newExpires.toISOString(),
}
@@ -128,7 +128,7 @@ export async function session(
user: {
name: user.name,
email: user.email,
image: user.image,
picture: user.image,
},
expires: session.expires.toISOString(),
},

View File

@@ -55,8 +55,9 @@ export async function signin(
} catch (e) {
const error = new SignInError(e as Error, { provider: provider.id })
logger.error(error)
url.searchParams.set("error", error.name)
url.pathname += "/error"
const code = provider.type === "email" ? "EmailSignin" : "OAuthSignin"
url.searchParams.set("error", code)
url.pathname += "/signin"
return { redirect: url.toString() }
}
}

View File

@@ -33,6 +33,8 @@ export async function toInternalRequest(
// TODO: url.toString() should not include action and providerId
// see init.ts
const url = new URL(req.url.replace(/\/$/, ""))
// FIXME: Upstream issue in Next.js, pathname segments get included as part of the query string
url.searchParams.delete("nextauth")
const { pathname } = url
const action = actions.find((a) => pathname.includes(a))

View File

@@ -29,7 +29,7 @@ export interface SendVerificationRequestParams {
export interface EmailConfig extends CommonProviderOptions {
type: "email"
// TODO: Make use of https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html
server: string | SMTPTransportOptions
server?: string | SMTPTransportOptions
/** @default `"Auth.js <no-reply@authjs.dev>"` */
from?: string
/**
@@ -72,7 +72,7 @@ export interface EmailConfig extends CommonProviderOptions {
* By default, we treat email addresses as all lower case,
* but you can override this function to change this behavior.
*
* [Documentation](https://authjs.dev/guides/providers/email#normalizing-the-e-mail-address) | [RFC 2821](https://tools.ietf.org/html/rfc2821) | [Email syntax](https://en.wikipedia.org/wiki/Email_address#Syntax)
* [Normalizing the email address](https://authjs.dev/reference/core/providers_email#normalizing-the-email-address) | [RFC 2821](https://tools.ietf.org/html/rfc2821) | [Email syntax](https://en.wikipedia.org/wiki/Email_address#Syntax)
*/
normalizeIdentifier?: (identifier: string) => string
}
@@ -287,7 +287,7 @@ export type EmailProviderType = "email"
*
* ## Normalizing the email address
*
* By default, NextAuth.js will normalize the email address. It treats values as case-insensitive (which is technically not compliant to the [RFC 2821 spec](https://datatracker.ietf.org/doc/html/rfc2821), but in practice this causes more problems than it solves, eg. when looking up users by e-mail from databases.) and also removes any secondary email address that was passed in as a comma-separated list. You can apply your own normalization via the `normalizeIdentifier` method on the `EmailProvider`. The following example shows the default behavior:
* By default, Auth.js will normalize the email address. It treats values as case-insensitive (which is technically not compliant to the [RFC 2821 spec](https://datatracker.ietf.org/doc/html/rfc2821), but in practice this causes more problems than it solves, eg. when looking up users by e-mail from databases.) and also removes any secondary email address that was passed in as a comma-separated list. You can apply your own normalization via the `normalizeIdentifier` method on the `EmailProvider`. The following example shows the default behavior:
* ```ts
* EmailProvider({
* // ...
@@ -301,7 +301,7 @@ export type EmailProviderType = "email"
* return `${local}@${domain}`
*
* // You can also throw an error, which will redirect the user
* // to the error page with error=EmailSignin in the URL
* // to the sign-in page with error=EmailSignin in the URL
* // if (identifier.split("@").length > 2) {
* // throw new Error("Only one email allowed")
* // }

View File

@@ -52,7 +52,7 @@ export interface EVEOnlineProfile extends Record<string, any> {
* :::
*
* :::tip
* If using JWT for the session, you can add the `CharacterID` to the JWT token and session. Example:
* If using JWT for the session, you can add the `CharacterID` to the JWT and session. Example:
* ```js
* options: {
* jwt: {

View File

@@ -65,6 +65,7 @@ export interface GitHubProfile {
space: number
private_repos: number
}
[claim: string]: unknown
}
/**

View File

@@ -52,7 +52,10 @@ interface AdvancedEndpointHandler<P extends UrlParams, C, R> {
conform?: (response: Response) => Awaitable<Response | undefined>
}
/** Either an URL (containing all the parameters) or an object with more granular control. */
/**
* Either an URL (containing all the parameters) or an object with more granular control.
* @internal
*/
export type EndpointHandler<
P extends UrlParams,
C = any,
@@ -92,6 +95,8 @@ export type ProfileCallback<Profile> = (
tokens: TokenSet
) => Awaitable<User>
export type AccountCallback = (account: TokenSet) => TokenSet
export interface OAuthProviderButtonStyles {
logo: string
logoDark: string
@@ -138,13 +143,25 @@ export interface OAuth2Config<Profile>
userinfo?: string | UserinfoEndpointHandler
type: "oauth"
/**
* Receives the profile object returned by the OAuth provider, and returns the user object.
* This will be used to create the user in the database.
* Receives the full {@link Profile} returned by the OAuth provider, and returns a subset.
* It is used to create the user in the database.
*
* Defaults to: `id`, `email`, `name`, `image`
*
* [Documentation](https://authjs.dev/reference/adapters/models#user)
* @see [Database Adapter: User model](https://authjs.dev/reference/adapters#user)
*/
profile?: ProfileCallback<Profile>
/**
* Receives the full {@link TokenSet} returned by the OAuth provider, and returns a subset.
* It is used to create the account associated with a user in the database.
*
* Defaults to: `access_token` and `id_token`
*
* @see [Database Adapter: Account model](https://authjs.dev/reference/adapters#account)
* @see https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
* @see https://www.ietf.org/rfc/rfc6749.html#section-5.1
*/
account?: AccountCallback
/**
* The CSRF protection performed on the callback endpoint.
* @default ["pkce"]
@@ -190,7 +207,11 @@ export interface OAuth2Config<Profile>
options?: OAuthUserConfig<Profile>
}
/** TODO: Document */
/**
* Extension of the {@link OAuth2Config}.
*
* @see https://openid.net/specs/openid-connect-core-1_0.html
*/
export interface OIDCConfig<Profile>
extends Omit<OAuth2Config<Profile>, "type" | "checks"> {
type: "oidc"
@@ -204,6 +225,7 @@ export type OAuthEndpointType = "authorization" | "token" | "userinfo"
/**
* We parsed `authorization`, `token` and `userinfo`
* to always contain a valid `URL`, with the params
* @internal
*/
export type OAuthConfigInternal<Profile> = Omit<
OAuthConfig<Profile>,
@@ -229,7 +251,10 @@ export type OAuthConfigInternal<Profile> = Omit<
*
*/
redirectProxyUrl?: OAuth2Config<Profile>["redirectProxyUrl"]
} & Pick<Required<OAuthConfig<Profile>>, "clientId" | "checks" | "profile">
} & Pick<
Required<OAuthConfig<Profile>>,
"clientId" | "checks" | "profile" | "account"
>
export type OIDCConfigInternal<Profile> = OAuthConfigInternal<Profile> & {
checks: OIDCConfig<Profile>["checks"]
@@ -238,11 +263,9 @@ export type OIDCConfigInternal<Profile> = OAuthConfigInternal<Profile> & {
export type OAuthUserConfig<Profile> = Omit<
Partial<OAuthConfig<Profile>>,
"options" | "type"
> &
Required<Pick<OAuthConfig<Profile>, "clientId" | "clientSecret">>
>
export type OIDCUserConfig<Profile> = Omit<
Partial<OIDCConfig<Profile>>,
"options" | "type"
> &
Required<Pick<OIDCConfig<Profile>, "clientId" | "clientSecret">>
>

View File

@@ -99,6 +99,7 @@ export interface TwitterProfile {
text: string
}>
}
[claims: string]: unknown
}
/**

View File

@@ -116,16 +116,58 @@ export interface Account extends Partial<OpenIDTokenEndpointResponse> {
providerAccountId: string
/** Provider's type for this account */
type: ProviderType
/** id of the user this account belongs to */
/**
* id of the user this account belongs to
*
* @see https://authjs.dev/reference/adapters#user
*/
userId?: string
/**
* Calculated value based on {@link OAuth2TokenEndpointResponse.expires_in}.
*
* It is the absolute timestamp (in seconds) when the {@link OAuth2TokenEndpointResponse.access_token} expires.
*
* This value can be used for implementing token rotation together with {@link OAuth2TokenEndpointResponse.refresh_token}.
*
* @see https://authjs.dev/guides/basics/refresh-token-rotation#database-strategy
* @see https://www.rfc-editor.org/rfc/rfc6749#section-5.1
*/
expires_at?: number
}
/** The OAuth profile returned from your provider */
/**
* The user info returned from your OAuth provider.
*
* @see https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
*/
export interface Profile {
sub?: string | null
name?: string | null
given_name?: string | null
family_name?: string | null
middle_name?: string | null
nickname?: string | null
preferred_username?: string | null
profile?: string | null
picture?: string | null | any
website?: string | null
email?: string | null
image?: string | null
email_verified?: boolean | null
gender?: string | null
birthdate?: string | null
zoneinfo?: string | null
locale?: string | null
phone_number?: string | null
updated_at?: Date | string | number | null
address?: {
formatted?: string | null
street_address?: string | null
locality?: string | null
region?: string | null
postal_code?: string | null
country?: string | null
} | null
[claim: string]: unknown
}
/** [Documentation](https://authjs.dev/guides/basics/callbacks) */
@@ -182,7 +224,7 @@ export interface CallbacksOptions<P = Profile, A = Account> {
* This callback is called whenever a session is checked.
* (Eg.: invoking the `/api/session` endpoint, using `useSession` or `getSession`)
*
* ⚠ By default, only a subset (email, name, image)
* ⚠ By default, only a subset (email, name, picture)
* of the token is returned for increased security.
*
* If you want to make something available you added to the token through the `jwt` callback,
@@ -262,7 +304,7 @@ export interface EventCallbacks {
/**
* The message object will contain one of these depending on
* if you use JWT or database persisted sessions:
* - `token`: The JWT token for this session.
* - `token`: The JWT for this session.
* - `session`: The session object from your adapter that is being ended.
*/
signOut: (
@@ -280,7 +322,7 @@ export interface EventCallbacks {
/**
* The message object will contain one of these depending on
* if you use JWT or database persisted sessions:
* - `token`: The JWT token for this session.
* - `token`: The JWT for this session.
* - `session`: The session object from your adapter.
*/
session: (message: { session: Session; token: JWT }) => Awaitable<void>
@@ -295,7 +337,7 @@ export type ErrorPageParam = "Configuration" | "AccessDenied" | "Verification"
export type SignInPageErrorParam =
| "Signin"
| "OAuthSignin"
| "OAuthCallback"
| "OAuthCallbackError"
| "OAuthCreateAccount"
| "EmailCreateAccount"
| "Callback"
@@ -335,7 +377,7 @@ export interface DefaultSession {
user?: {
name?: string | null
email?: string | null
image?: string | null
picture?: string | null
}
expires: ISODateString
}
@@ -385,15 +427,40 @@ export type InternalProvider<T = ProviderType> = (T extends "oauth"
callbackUrl: string
}
/**
* Supported actions by Auth.js. Each action map to a REST API endpoint.
* Some actions have a `GET` and `POST` variant, depending on if the action
* changes the state of the server.
*
* - **`"callback"`**:
* - **`GET`**: Handles the callback from an [OAuth provider](https://authjs.dev/reference/core/providers_oauth).
* - **`POST`**: Handles the callback from a [Credentials provider](https://authjs.dev/reference/core/providers_credentials).
* - **`"csrf"`**: Returns the raw CSRF token, which is saved in a cookie (encrypted).
* It is used for CSRF protection, implementing the [double submit cookie](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie) technique.
* :::note
* Some frameworks have built-in CSRF protection and can therefore disable this action. In this case, the corresponding endpoint will return a 404 response. Read more at [`skipCSRFCheck`](https://authjs.dev/reference/core#skipcsrfcheck).
* _⚠ We don't recommend manually disabling CSRF protection, unless you know what you're doing._
* :::
* - **`"error"`**: Renders the built-in error page.
* - **`"providers"`**: Returns a client-safe list of all configured providers.
* - **`"session"`**: Returns the user's session if it exists, otherwise `null`.
* - **`"signin"`**:
* - **`GET`**: Renders the built-in sign-in page.
* - **`POST`**: Initiates the sign-in flow.
* - **`"signout"`**:
* - **`GET`**: Renders the built-in sign-out page.
* - **`POST`**: Initiates the sign-out flow. This will invalidate the user's session (deleting the cookie, and if there is a session in the database, it will be deleted as well).
* - **`"verify-request"`**: Renders the built-in verification request page.
*/
export type AuthAction =
| "callback"
| "csrf"
| "error"
| "providers"
| "session"
| "csrf"
| "signin"
| "signout"
| "callback"
| "verify-request"
| "error"
/** @internal */
export interface RequestInternal {

View File

@@ -0,0 +1,24 @@
<p align="center">
<br/>
<a href="https://authjs.dev" target="_blank"><img width="150px" src="https://authjs.dev/img/logo/logo-sm.png" /></a>
<h3 align="center">NextAuth.js</a></h3>
<h4 align="center">Authentication for Next.js.</h4>
<p align="center" style="align: center;">
<a href="https://npm.im/next-auth">
<img src="https://img.shields.io/badge/TypeScript-blue?style=flat-square" alt="TypeScript" />
</a>
<a href="https://npm.im/next-auth">
<img alt="npm" src="https://img.shields.io/npm/v/next-auth?color=green&label=next-auth&style=flat-square">
</a>
<a href="https://www.npmtrends.com/next-auth">
<img src="https://img.shields.io/npm/dm/next-auth?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 [nextjs.authjs.dev](https://nextjs.authjs.dev).

View File

@@ -0,0 +1,75 @@
{
"name": "@auth/nextjs",
"version": "0.0.1",
"description": "Authentication for Next.js",
"homepage": "https://nextjs.authjs.dev",
"repository": "https://github.com/nextauthjs/next-auth.git",
"author": "Balázs Orbán <info@balazsorban.com>",
"keywords": [
"react",
"nodejs",
"oauth",
"jwt",
"oauth2",
"authentication",
"nextjs",
"csrf",
"oidc",
"nextauth"
],
"type": "module",
"types": "./index.d.ts",
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.js"
},
"./adapters": {
"types": "./adapters.d.ts"
},
"./jwt": {
"types": "./jwt.d.ts",
"import": "./jwt.js"
},
"./middleware": {
"types": "./middleware.d.ts",
"import": "./middleware.js"
},
"./next": {
"types": "./next.d.ts",
"import": "./next.js"
},
"./providers": {
"types": "./providers.d.ts"
},
"./react": {
"types": "./react.d.ts",
"import": "./react.js"
},
"./package.json": "./package.json"
},
"scripts": {
"dev": "tsc -w",
"clean": "rm -rf *.js *.d.ts lib",
"build": "pnpm clean && tsc"
},
"files": [
"*.js",
"*.d.ts",
"lib",
"src"
],
"devDependencies": {
"@types/react": "18.0.37",
"typescript": "^4",
"next": "13.4.0"
},
"license": "ISC",
"dependencies": {
"@auth/core": "workspace:*"
},
"peerDependencies": {
"next": "^13.4.0",
"react": "^18.2.0"
}
}

Some files were not shown because too many files have changed in this diff Show More