Compare commits

..

29 Commits

Author SHA1 Message Date
Balázs Orbán
163d8c66e2 chore: bump version 2022-07-01 13:53:57 +02:00
Balázs Orbán
5319dca583 fix(ts): fall back to empty string when parsing cookie 2022-07-01 12:14:48 +02:00
Balázs Orbán
cd6ccfde89 fix(core): handle invalid email 2022-07-01 12:09:57 +02:00
Nico Domino
89d91ea282 chore: update docs regarding new server session API (#4776)
* chore: update docs regarding new server session API

* fix: add note about unstable_ API prefix

* Apply suggestions from code review

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-06-30 05:18:52 -04:00
Balázs Orbán
ca3165bd5a Update README.md 2022-06-29 15:55:54 +02:00
Balázs Orbán
aa527b37bf Update README.md 2022-06-29 15:54:49 +02:00
Richard
f3233641d0 docs: not -> now (#4783)
I assume this is meant to say "is now secured" ?
2022-06-28 19:12:48 +02:00
Balázs Orbán
4bee970775 chore: bump version 2022-06-28 18:28:53 +02:00
Balázs Orbán
80a4f50be2 chore: upgrade Docusaurus (#4782) 2022-06-28 12:55:43 +02:00
Ofek Asido
1f4ffbaefe docs: wrong semicolon placement (#4781) 2022-06-28 12:45:48 +02:00
Balázs Orbán
a911b4a40b chore(deps): upgrade dependencies 2022-06-28 12:22:11 +02:00
Balázs Orbán
cb0f3e1ae2 chore: rename GITHUB_TOKEN to RELEASE_TOKEN 2022-06-28 12:21:58 +02:00
Balázs Orbán
c194261617 fix(core): respect NEXTAUTH_SECRET in unstable_getServerSession (#4774)
* fix(core): respect `NEXTAUTH_SECRET` in `unstable_getServerSession`

* add `secret` tests

* add `@types/jest`

* fix tests
2022-06-27 17:00:08 +02:00
Nico Domino
5fdd8483d8 chore: add security headers to docs vercel.json (#4766) 2022-06-24 14:30:48 +02:00
Balázs Orbán
99f5b9616f fix: update Middleware (#4757)
* merge `main`, fix workspace dependencies

* chore(dev): use `matcher` in Middleware

* fix(middleware): support `cookies` as `Map`

* simplify

* chore(example): use new Middleware API

* chore(example): use `next-auth@latest`

* docs(middleware): document new Middleware API

* docs(ts): update inline example

* fix(ts): make cookies optional

* remove non-null assertion
2022-06-24 14:11:39 +02:00
Michael Hays
d8d9ab94cb feat(core): pass profile to linkAccount event (#4242)
Co-authored-by: Nico Domino <yo@ndo.dev>
2022-06-24 12:03:20 +02:00
Nico Domino
e8827cbf45 chore(docs): update securing pages tutorial (#3982)
Co-authored-by: Lluis Agusti <hi@llu.lu>
Co-authored-by: Thang Vu <31528554+ThangHuuVu@users.noreply.github.com>
2022-06-24 10:02:26 +02:00
Balázs Orbán
37c4a813e3 chore: bump version 2022-06-23 16:48:10 +02:00
Balázs Orbán
6a23ff7126 fix(build): include utils in package (#4760)
* fix: include `utils` in package

* fix: import `InternalUrl` as type

* `"emitDeclarationOnly": false`

* don't ignore `types.ts` in build
2022-06-23 16:41:01 +02:00
Balázs Orbán
23db0e68dd chore: bump version 2022-06-23 12:30:56 +02:00
Arthur Pedroti
e03e234b86 fix(ts): infer provider type in signIn (#4679)
* fix: signIn infer provider type

The "P" type it's not passed in any props, so the result type doesn't understand and return the false type always, Adding the "P" at provider type props.

* fix: P possibly undefined

Co-authored-by: arthurpedroti@gmail.com <arthurpedroti@LAPTOP-MVAK9RM5.localdomain>
2022-06-23 12:18:32 +02:00
Thang Vu
66fb914a31 feat: introduce experimental unstable_getServerSession API (#4116)
* refactor: improve `getServerSession` API

* Apply review comment

Co-authored-by: Balázs Orbán <info@balazsorban.com>

* Apply review comment

Co-authored-by: Balázs Orbán <info@balazsorban.com>

* Apply review comment

Co-authored-by: Balázs Orbán <info@balazsorban.com>

* Apply review comment

Co-authored-by: Balázs Orbán <info@balazsorban.com>

* Apply review comment

Co-authored-by: Balázs Orbán <info@balazsorban.com>

* Apply review comment

Co-authored-by: Balázs Orbán <info@balazsorban.com>

* Address docs review

* Fix a typo

* Update lint file location

* Address review comments

* getServerSession -> unstable_getServerSession

* Apply suggestions from code review

* Apply suggestions from code review

Co-authored-by: Dragate <spidfair@gmail.com>

* Update packages/next-auth/package.json

Co-authored-by: Balázs Orbán <info@balazsorban.com>
Co-authored-by: Dragate <spidfair@gmail.com>
2022-06-23 12:15:15 +02:00
Balázs Orbán
8ce728197f chore: update to use release token 2022-06-23 12:10:05 +02:00
ml4den
87d1a7af6d docs(providers): Change env names (#4753)
Proposing to match the env names to what is in the example [...nextauth].ts
2022-06-22 22:23:04 +02:00
Matthew Francis Brunetti
172813f987 docs(adapters): fix references to deprecated adapters repo (#4737) 2022-06-22 18:12:07 +02:00
dependabot[bot]
cc934fceec chore(deps): bump next-auth in /apps/playground-sveltekit (#4746)
Bumps [next-auth](https://github.com/nextauthjs/next-auth) from 4.3.3 to 4.5.0.
- [Release notes](https://github.com/nextauthjs/next-auth/releases)
- [Changelog](https://github.com/nextauthjs/next-auth/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nextauthjs/next-auth/compare/next-auth@v4.3.3...next-auth@v4.5.0)

---
updated-dependencies:
- dependency-name: next-auth
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-22 18:07:17 +02:00
Guillermo Villegas Gray
46e467a7cb docs(providers): typo in slack.md (#4733)
typo in slack.md, thanks for the warning though!
2022-06-22 18:06:59 +02:00
Gal Schlezinger
73d489beac fix(edge): support request.cookies as a map (#4745)
in next Next.js versions, NextRequest.cookies will be an instance of NextCookies which is
some kind of a Map, instead of a plain object.

This commit checks whether there's a `get` function in req.cookies, and acts accordingly,
to make sure we will support newer Next.js versions with Edge Functions/Middleware
2022-06-21 20:20:48 +02:00
Balázs Orbán
e498483b23 test: add test for invalid callbackUrl handling 2022-06-20 10:38:21 +02:00
87 changed files with 1569 additions and 1636 deletions

View File

@@ -69,7 +69,7 @@ jobs:
git config --global user.name "Balázs Orbán"
pnpm release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
NPM_TOKEN_PKG: ${{ secrets.NPM_TOKEN_PKG }}
NPM_TOKEN_ORG: ${{ secrets.NPM_TOKEN_ORG }}
release-pr:

View File

@@ -1,5 +1,7 @@
export { default } from "next-auth/middleware"
export const config = { matcher: ["/middleware-protected"] }
// Other ways to use this middleware
// import withAuth from "next-auth/middleware"

View File

@@ -16,21 +16,21 @@
},
"license": "ISC",
"dependencies": {
"@next-auth/fauna-adapter": "^1.0.1",
"@next-auth/prisma-adapter": "^1.0.1",
"@prisma/client": "^3.10.0",
"cpx": "^1.5.0",
"fake-smtp-server": "^0.8.0",
"faunadb": "^4.4.1",
"next": "^12.1.0",
"nodemailer": "^6.7.2",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"@next-auth/fauna-adapter": "^1",
"@next-auth/prisma-adapter": "^1",
"@prisma/client": "^3",
"faunadb": "^4",
"next": "12.1.7-canary.51",
"nodemailer": "^6",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@types/react": "^17.0.37",
"@types/react-dom": "^17.0.11",
"concurrently": "^7.1.0",
"prisma": "^3.10.0"
"@types/react": "^18",
"@types/react-dom": "^18",
"concurrently": "^7",
"cpx": "^1.5.0",
"fake-smtp-server": "^0.8.0",
"prisma": "^3"
}
}

View File

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

View File

@@ -1,7 +1,7 @@
// This is an example of how to access a session from an API route
import { getSession } from "next-auth/react"
import { unstable_getServerSession } from "next-auth/next"
export default async (req, res) => {
const session = await getSession({ req })
const session = await unstable_getServerSession(req, res, authOptions)
res.send(JSON.stringify(session, null, 2))
}

View File

@@ -1,5 +1,5 @@
// This is an example of how to protect content using server rendering
import { getServerSession } from "next-auth/next"
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "./api/auth/[...nextauth]"
import Layout from "../components/layout"
import AccessDenied from "../components/access-denied"
@@ -26,7 +26,11 @@ export default function Page({ content, session }) {
}
export async function getServerSideProps(context) {
const session = await getServerSession(context, authOptions)
const session = await unstable_getServerSession(
context.req,
context.res,
authOptions
)
let content = null
if (session) {

View File

@@ -1,4 +1,4 @@
import { getSession } from "next-auth/react"
import { unstable_getServerSession } from "next-auth/next"
import Layout from "../components/layout"
export default function Page() {
@@ -11,13 +11,17 @@ export default function Page() {
<Layout>
<h1>Server Side Rendering</h1>
<p>
This page uses the universal <strong>getSession()</strong> method in{" "}
<strong>getServerSideProps()</strong>.
This page uses the <strong>unstable_getServerSession()</strong> method
in <strong>getServerSideProps()</strong>.
</p>
<p>
Using <strong>getSession()</strong> in{" "}
<strong>getServerSideProps()</strong> is the recommended approach if you
need to support Server Side Rendering with authentication.
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
@@ -35,7 +39,11 @@ export default function Page() {
export async function getServerSideProps(context) {
return {
props: {
session: await getSession(context),
session: await unstable_getServerSession(
contex.req,
contex.res,
authOptions
),
},
}
}

View File

@@ -65,7 +65,6 @@ 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)
* Adapters Repo: [nextauthjs/adapters](https://github.com/nextauthjs/adapters)
### 3. Configure Authentication Providers

View File

@@ -12,9 +12,9 @@
"dependencies": {
"dotenv": "^16.0.0",
"gatsby": "next",
"next-auth": "^4.2.1",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"next-auth": "latest",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"vercel": "^23.1.2"

View File

@@ -68,7 +68,6 @@ 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)
* Adapters Repo: [nextauthjs/adapters](https://github.com/nextauthjs/adapters)
### 3. Configure Authentication Providers

View File

@@ -0,0 +1,12 @@
import { withAuth } from "next-auth/middleware"
// 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, but /me only requires the user to be logged in.
req.nextUrl.pathname !== "/admin" || token?.userRole === "admin",
},
})
export const config = { matcher: ["/admin", "/me"] }

View File

@@ -23,16 +23,16 @@
],
"license": "ISC",
"dependencies": {
"next": "^12.0.11-canary.4",
"next": "12.1.7-canary.51",
"next-auth": "latest",
"nodemailer": "^6.6.3",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"nodemailer": "^6",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^17.0.14",
"@types/react": "^17.0.39",
"typescript": "^4.5.5"
"@types/node": "^17",
"@types/react": "^18",
"typescript": "^4"
},
"prettier": {
"semi": false

View File

@@ -1,4 +1,4 @@
import Layout from "../../components/layout"
import Layout from "../components/layout"
export default function Page() {
return (

View File

@@ -1,8 +0,0 @@
import { withAuth } from "next-auth/middleware"
// More on how NextAuth.js middleware works: https://next-auth.js.org/configuration/nextjs#middleware
export default withAuth({
callbacks: {
authorized: ({ token }) => token?.userRole === "admin",
},
})

View File

@@ -1,4 +1,4 @@
import NextAuth from "next-auth"
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"
@@ -9,7 +9,7 @@ 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 default NextAuth({
export const authOptions: NextAuthOptions = {
// https://next-auth.js.org/configuration/providers/oauth
providers: [
/* EmailProvider({
@@ -18,7 +18,7 @@ export default NextAuth({
}),
// Temporarily removing the Apple provider from the demo site as the
// callback URL for it needs updating due to Vercel changing domains
Providers.Apple({
clientId: process.env.APPLE_ID,
clientSecret: {
@@ -60,4 +60,6 @@ export default NextAuth({
return token
},
},
})
}
export default NextAuth(authOptions)

View File

@@ -1,9 +1,9 @@
// This is an example of to protect an API route
import { getSession } from "next-auth/react"
import { unstable_getServerSession } from "next-auth/next"
import type { NextApiRequest, NextApiResponse } from "next"
export default async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getSession({ req })
const session = await unstable_getServerSession(req, res, authOptions)
if (session) {
res.send({
@@ -12,7 +12,8 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
})
} else {
res.send({
error: "You must be signed in to view the protected content on this page.",
error:
"You must be signed in to view the protected content on this page.",
})
}
}

View File

@@ -1,5 +1,5 @@
import { useSession } from "next-auth/react"
import Layout from "../../components/layout"
import Layout from "../components/layout"
export default function MePage() {
const { data } = useSession()

View File

@@ -1,2 +0,0 @@
// More on how NextAuth.js middleware works: https://next-auth.js.org/configuration/nextjs#middleware
export { default } from "next-auth/middleware"

View File

@@ -1,26 +1,24 @@
import { useSession, getSession } from "next-auth/react"
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "./api/auth/[...nextauth]"
import Layout from "../components/layout"
import type { NextPageContext } from "next"
export default function ServerSidePage() {
export default function ServerSidePage({ session }) {
// 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()`.
const { data: session, status } = useSession()
const loading = status === "loading"
return (
<Layout>
<h1>Server Side Rendering</h1>
<p>
This page uses the universal <strong>getSession()</strong> method in{" "}
<strong>getServerSideProps()</strong>.
This page uses the <strong>unstable_getServerSession()</strong> method
in <strong>unstable_getServerSideProps()</strong>.
</p>
<p>
Using <strong>getSession()</strong> in{" "}
<strong>getServerSideProps()</strong> is the recommended approach if you
need to support Server Side Rendering with authentication.
Using <strong>unstable_getServerSession()</strong> in{" "}
<strong>unstable_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
@@ -38,7 +36,7 @@ export default function ServerSidePage() {
export async function getServerSideProps(context: NextPageContext) {
return {
props: {
session: await getSession(context),
session: await unstable_getServerSession(context.req, context.res, authOptions),
},
}
}

View File

@@ -30,7 +30,7 @@
"type": "module",
"dependencies": {
"cookie": "0.4.1",
"next-auth": "^4.3.3"
"next-auth": "workspace:*"
},
"prettier": {
"semi": false,

View File

@@ -1232,10 +1232,10 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
next-auth@^4.3.3:
version "4.3.3"
resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-4.3.3.tgz#5ff892e73648a0f33c2af0e9d7cafda729f63ae7"
integrity sha512-bUs+oOOPT18Pq/+4v9q4PA/DGoVoAX6jwY7RTfE/akFXwlny+y/mNS6lPSUwpqcHjljqBaq34PQA3+01SdOOPw==
next-auth@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-4.5.0.tgz#2df57287fddc705b8971c88c60bad44a89ac6dd1"
integrity sha512-B6gYRIbqtj8nlDsx3y2Ruwp/mvZnItPs7VUULY43QYw+M9xtDPIM9EBZ3ryd/wNYA3MDteBJlzGm/ivseXcmJA==
dependencies:
"@babel/runtime" "^7.16.3"
"@panva/hkdf" "^1.0.1"

View File

@@ -236,7 +236,7 @@ export default NextAuth({
encode: async ({ secret, token }) => {
return jwt.sign({...token, userId: token.id}, secret, {
algorithm: "HS256",
expiresIn: 30 * 24 * 60 * 60; // 30 days
expiresIn: 30 * 24 * 60 * 60, // 30 days
});
},
decode: async ({ secret, token }) => {

View File

@@ -107,7 +107,7 @@ The redirect callback may be invoked more than once in the same flow.
This callback is called whenever a JSON Web Token is created (i.e. at sign
in) or updated (i.e whenever a session is accessed in the client). The returned value will be [encrypted](/configuration/options#jwt), and it is stored in a cookie.
Requests to `/api/auth/signin`, `/api/auth/session` and calls to `getSession()`, `useSession()` will invoke this function, but only if you are using a [JWT session](/configuration/options#session). This method is not invoked when you persist sessions in a database.
Requests to `/api/auth/signin`, `/api/auth/session` and calls to `getSession()`, `unstable_getServerSession()`, `useSession()` will invoke this function, but only if you are using a [JWT session](/configuration/options#session). This method is not invoked when you persist sessions in a database.
- As with database persisted session expiry times, token expiry time is extended whenever a session is active.
- The arguments _user_, _account_, _profile_ and _isNewUser_ are only passed the first time this callback is called on a new session, after the user signs in. In subsequent calls, only `token` will be available.

View File

@@ -53,6 +53,7 @@ The message object will contain:
- `user`: The user object from your adapter.
- `account`: The object returned from the provider.
- `profile`: The object returned from the `profile` callback of the OAuth provider.
### session

View File

@@ -1,5 +1,73 @@
# Next.js
## `unstable_getServerSession`
:::warning
This feature is experimental and may be removed or changed in the future.
:::
When calling from server-side i.e. in API routes or in `getServerSideProps`, we recommend using this function instead of `getSession` to retrieve the `session` object. This method is especially useful when you are using NextAuth.js with a database. This method can _drastically_ reduce response time when used over `getSession` server-side, due to avoiding an extra `fetch` to an API Route (this is generally [not recommended in Next.js](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props#getserversideprops-or-api-routes)). In addition, `unstable_getServerSession` will correctly update the cookie expiry time and update the session content if `callbacks.jwt` or `callbacks.session` changed something.
Otherwise, if you only want to get the session token, see [`getToken`](tutorials/securing-pages-and-api-routes#using-gettoken).
`unstable_getServerSession` requires passing the same object you would pass to `NextAuth` when initializing NextAuth.js. To do so, you can export your NextAuth.js options in the following way:
In `[...nextauth.js]`:
```ts
import { NextAuth } from 'next-auth'
import type { NextAuthOptions } from 'next-auth'
export const authOptions: NextAuthOptions = {
// your configs
}
export default NextAuth(authOptions);
```
In `getServerSideProps`:
```js
import { authOptions } from 'pages/api/[...nextauth]'
import { unstable_getServerSession } from "next-auth/next"
export async function getServerSideProps(context) {
const session = await unstable_getServerSession(context.req, context.res, authOptions)
if (!session) {
return {
redirect: {
destination: '/',
permanent: false,
},
}
}
return {
props: {
session,
},
}
}
```
In API routes:
```js
import { authOptions } from 'pages/api/[...nextauth]'
import { unstable_getServerSession } from "next-auth/next"
export async function handler(req, res) {
const session = await unstable_getServerSession(req, res, authOptions)
if (!session) {
res.status(401).json({ message: "You must be logged in." });
return;
}
return res.json({
message: 'Success',
})
}
```
## Middleware
You can use a Next.js Middleware with NextAuth.js to protect your site.
@@ -18,50 +86,27 @@ You must set the [`NEXTAUTH_SECRET`](/configuration/options#nextauth_secret) env
### Basic usage
The most simple usage is when you want to require authentication for your entire site. You can add a `middleware.js` file with the following:
```js
import withAuth from "next-auth/middleware"
// or
import { withAuth } from "next-auth/middleware"
export { default } from "next-auth/middleware"
```
### Custom JWT decode method
That's it! Your application is now secured. 🎉
If you have custom jwt decode method set in `[...nextauth].ts`, you must also pass the same `decode` method to `withAuth` in order to read the custom-signed JWT correctly. You may want to extract the encode/decode logic to a separate function for consistency.
If you only want to secure certain pages, export a `config` object with a `matcher`:
`[...nextauth].ts`
```ts
import jwt from "jsonwebtoken";
```js
export { default } from "next-auth/middleware"
export default NextAuth({
providers: [...],
secret: /* Please use `process.env.NEXTAUTH_SECRET` */,
jwt: {
encode: async ({ secret, token }) => {
return jwt.sign(token as any, secret);
},
decode: async ({ secret, token }) => {
return jwt.verify(token as string, secret) as any;
},
},
})
export const config = { matcher: ["/dashboard"] }
```
Any `_middleware.ts`
```ts
import withAuth from "next-auth/middleware"
import jwt from "jsonwebtoken";
Now you will still be able to visit every page, but only `/dashboard` will require authentication.
If a user is not logged in, the default behavior is to redirect them to the sign-in page.
export default withAuth({
jwt: {
decode: async ({ secret, token }) => {
return jwt.verify(token, secret) as any;
},
},
callbacks: {
authorized: ({ token }) => !!token,
},
})
```
---
### `callbacks`
@@ -104,46 +149,24 @@ See the documentation for the [pages option](/configuration/pages) for more info
---
### Examples
### Advanced usage
`withAuth` is very flexible, there are multiple ways to use it.
NextAuth.js Middleware is very flexible, there are multiple ways to use it.
:::note
If you do not define the options, NextAuth.js will use the default values for the omitted options.
:::
#### default re-export
```js title="pages/_middleware.js"
export { default } from "next-auth/middleware"
```
With this one line, when someone tries to load any of your pages, they will have to be logged-in first. Otherwise, they are redirected to the login page. It will assume that you are using the `NEXTAUTH_SECRET` environment variable.
#### default `withAuth` export
```js title="pages/admin/_middleware.js"
import { withAuth } from "next-auth/middleware"
export default withAuth({
callbacks: {
authorized: ({ token }) => token?.role === "admin",
},
})
```
With the above code, you just made sure that only user's with the `admin` role can access any of the pages under the `/admin` route. (Including nested routes as well, like `/admin/settings` etc.).
#### wrap middleware
```ts title="pages/admin/_middleware.ts"
```ts title="middleware.ts"
import type { NextRequest } from "next/server"
import type { JWT } from "next-auth/jwt"
import { withAuth } from "next-auth/middleware"
export default withAuth(
function middleware(req: NextRequest & { nextauth: { token: JWT } }) {
// `withAuth` can augment your Request with the user's token.
function middleware(req: NextRequest & { nextauth: { token: JWT | null } }) {
console.log(req.nextauth.token)
},
{
@@ -152,12 +175,53 @@ export default withAuth(
},
}
)
export const config = { matcher: ["/admin"] }
```
The `middleware` function will only be invoked if the `authorized` callback returns `true`.
---
#### Custom JWT decode method
If you have a custom jwt decode method set in `[...nextauth].ts`, you must also pass the same `decode` method to `withAuth` in order to read the custom-signed JWT correctly. You may want to extract the encode/decode logic to a separate function for consistency.
``
```ts title="/api/auth/[...nextauth].ts"
import type { NextAuthOptions } from "next-auth"
import NextAuth from "next-auth"
import jwt from "jsonwebtoken"
export const authOptions: NextAuthOptions = {
providers: [...],
jwt: {
async encode({ secret, token }) {
return jwt.sign(token, secret)
},
async decode({ secret, token }) {
return jwt.verify(token, secret)
},
},
}
export default NextAuth(authOptions)
```
And:
```ts title="middleware.ts"
import withAuth from "next-auth/middleware"
import { authOptions } from "pages/api/auth/[...nextauth]";
export default withAuth({
jwt: { decode: authOptions.jwt },
callbacks: {
authorized: ({ token }) => !!token,
},
})
```
### Caveats
- Currently only supports session verification, as parts of the sign-in code need to run in a Node.js environment. In the future, we would like to make sure that NextAuth.js can fully run at the [Edge](https://nextjs.org/docs/api-reference/edge-runtime)

View File

@@ -126,10 +126,10 @@ function Auth({ children }) {
// if `{ required: true }` is supplied, `status` can only be "loading" or "authenticated"
const { status } = useSession({ required: true })
if (status === 'loading') {
if (status === "loading") {
return <div>Loading...</div>
}
return children
}
```
@@ -161,13 +161,19 @@ See repository [`README`](https://github.com/nextauthjs/react-query) for more de
## getSession()
- Client Side: **Yes**
- Server Side: **Yes**
- Server Side: **No** (See: [`unstable_getServerSession()`](/configuration/nextjs#unstable_getserversession)
NextAuth.js provides a `getSession()` method which can be called client or server side to return a session.
NextAuth.js provides a `getSession()` helper which should be called **client side only** to return the current active session.
It calls `/api/auth/session` and returns a promise with a session object, or null if no session exists.
On the server side, **this is still available to use**, however, we recommend using `unstable_getServerSession` going forward. The idea behind this is to avoid an additional unnecessary `fetch` call on the server side. For more information, please check out [this issue](https://github.com/nextauthjs/next-auth/issues/1535).
#### Client Side Example
:::note
The `unstable_getServerSession` only has the prefix `unstable_` at the moment, because the API may change in the future. There are no known bugs at the moment and it is safe to use. If you discover any issues, please do report them as a [GitHub Issue](https://github.com/nextauthjs/next-auth/issues) and we will patch them as soon as possible.
:::
This helper is helpful in case you want to read the session outside of the context of React.
When called, `getSession()` will send a request to `/api/auth/session` and returns a promise with a [session object](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/core/types.ts#L407-L425), or `null` if no session exists.
```js
async function myFunction() {
@@ -176,23 +182,7 @@ async function myFunction() {
}
```
#### Server Side Example
```js
import { getSession } from "next-auth/react"
export default async (req, res) => {
const session = await getSession({ req })
/* ... */
res.end()
}
```
:::note
When calling `getSession()` server side, you need to pass `{req}` or `context` object.
:::
The tutorial [securing pages and API routes](/tutorials/securing-pages-and-api-routes) shows how to use `getSession()` in server side calls.
Read the tutorial [securing pages and API routes](/tutorials/securing-pages-and-api-routes) to know how to fetch the session in server side calls using `unstable_getServerSession()`.
---
@@ -254,7 +244,7 @@ export default async (req, res) => {
```
:::note
Unlike `getSession()` and `getCsrfToken()`, when calling `getProviders()` server side, you don't need to pass anything, just as calling it client side.
Unlike and `getCsrfToken()`, when calling `getProviders()` server side, you don't need to pass anything, just as calling it client side.
:::
---
@@ -436,14 +426,14 @@ If you pass the `session` page prop to the `<SessionProvider>` as in the exa
This only works on pages where you provide the correct `pageProps`, however. This is normally done in `getInitialProps` or `getServerSideProps` on an individual page basis like so:
```js title="pages/index.js"
import { getSession } from "next-auth/react"
import { unstable_getServerSession } from "next-auth/next"
...
export async function getServerSideProps(ctx) {
return {
props: {
session: await getSession(ctx)
session: await unstable_getServerSession(ctx)
}
}
}
@@ -455,7 +445,7 @@ If every one of your pages needs to be protected, you can do this in `getInitial
The session state is automatically synchronized across all open tabs/windows and they are all updated whenever they gain or lose focus or the state changes (e.g. a user signs in or out) when `refetchOnWindowFocus` is `true`.
If you have session expiry times of 30 days (the default) or more then you probably don't need to change any of the default options in the Provider. If you need to, you can trigger an update of the session object across all tabs/windows by calling `getSession()` from a client side function.
If you have session expiry times of 30 days (the default) or more then you probably don't need to change any of the default options in the Provider. If you need to, you can trigger an update of the session object across all tabs/windows by calling [`getSession()`](/getting-started/client#getsession) from a client side function.
However, if you need to customize the session behavior and/or are using short session expiry times, you can pass options to the provider to customize the behavior of the `useSession()` hook.

View File

@@ -93,13 +93,13 @@ You can use the `useSession` hook from anywhere in your application (e.g. in a h
### Backend - API Route
To protect an API Route, you can use the [`getSession()`](/getting-started/client#getsession) method in the NextAuth.js client.
To protect an API Route, you can use the [`unstable_getServerSession()`](/configuration/nextjs#unstable_getserversession) method.
```javascript title="pages/api/restricted.js" showLineNumbers
import { getSession } from "next-auth/react"
import { unstable_getServerSession } from "next-auth/next"
export default async (req, res) => {
const session = await getSession({ req })
const session = await unstable_getServerSession(req, res, authOptions)
if (session) {
res.send({
@@ -143,7 +143,7 @@ callbacks: {
...
```
Now whenever you call `getSession` or `useSession`, the data object which is returned will include the `accessToken` value.
Now whenever you call [`getSession`](/getting-started/client#getsession) or [`useSession`](/getting-started/client#usesession), the data object which is returned will include the `accessToken` value.
```jsx title="components/accessToken.jsx" showLineNumbers
import { useSession, signIn, signOut } from "next-auth/react"

View File

@@ -30,8 +30,8 @@ import GitHubProvider from "next-auth/providers/github";
...
providers: [
GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET
})
]
...

View File

@@ -13,7 +13,7 @@ https://api.slack.com/docs/sign-in-with-slack
https://api.slack.com/apps
:::warning
Slack requires you that the redirect URL of your app uses `https`, even for local development. An easy workaround for this is using a service like [`ngrok`](https://ngrok.com) that creates a secure tunnel to your app, using `https`. Remember to set the url as `NEXTAUTH_URL` as well.
Slack requires that the redirect URL of your app uses `https`, even for local development. An easy workaround for this is using a service like [`ngrok`](https://ngrok.com) that creates a secure tunnel to your app, using `https`. Remember to set the url as `NEXTAUTH_URL` as well.
:::
![](https://i.imgur.com/ydYKTLD.png)

View File

@@ -40,12 +40,28 @@ export default function Page() {
}
```
### Next.js (Middleware)
With NextAuth.js 4.2.0 and Next.js 12, you can now protect your pages via the middleware pattern more easily. If you would like to protect all pages, you can create a `_middleware.js` file in your root `pages` directory which looks like this.
```js title="/pages/_middleware.js"
export { default } from "next-auth/middleware"
```
Otherwise, if you only want to protect a subset of pages, you could put it in a subdirectory as well, for example in `/pages/admin/_middleware.js` would protect all pages under `/admin`.
For the time being, the `withAuth` middleware only supports `"jwt"` as [session strategy](https://next-auth.js.org/configuration/options#session).
More details can be found [here](https://next-auth.js.org/configuration/nextjs#middleware).
### Server Side
You can protect server side rendered pages using the `getSession()` method.
You can protect server side rendered pages using the `unstable_getServerSession` method. This is different from the old `getSession()` method, in that it does not do an extra fetch out over the internet to confirm data from itself, increasing performance significantly.
You need to add this to every server rendered page you want to protect. Be aware, `unstable_getServerSession` takes slightly different arguments than the method it is replacing, `getSession`.
```js title="pages/server-side-example.js"
import { useSession, getSession } from "next-auth/react"
import { useSession, unstable_getServerSession } from "next-auth/next"
export default function Page() {
const { data: session } = useSession()
@@ -66,7 +82,11 @@ export default function Page() {
export async function getServerSideProps(context) {
return {
props: {
session: await getSession(context),
session: await unstable_getServerSession(
context.req,
context.res,
authOptions
),
},
}
}
@@ -94,15 +114,15 @@ export default function App({
## Securing API Routes
### Using getSession()
### Using unstable_getServerSession()
You can protect API routes using the `getSession()` method.
You can protect API routes using the `unstable_getServerSession()` method.
```js title="pages/api/get-session-example.js"
import { getSession } from "next-auth/react"
import { unstable_getServerSession } from "next-auth/next"
export default async (req, res) => {
const session = await getSession({ req })
const session = await unstable_getServerSession(req, res, authOptions)
if (session) {
// Signed in
console.log("Session", JSON.stringify(session, null, 2))

View File

@@ -33,6 +33,10 @@ In development, we generate a `secret` based on your configuration for convenien
Twitter OAuth 2.0 is currently in beta as certain changes might still be necessary. This is not covered by semver. See the docs https://next-auth.js.org/providers/twitter#oauth-2
#### EXPERIMENTAL_API
Some APIs are still experimental; they may be changed or removed in the future. Use at your own risk.
## Adapter
### ADAPTER_TYPEORM_UPDATING_ENTITIES

View File

@@ -19,10 +19,10 @@
"generate-providers": "node ./scripts/generate-providers.js"
},
"dependencies": {
"@docusaurus/core": "^2.0.0-beta.20",
"@docusaurus/preset-classic": "^2.0.0-beta.20",
"@docusaurus/remark-plugin-npm2yarn": "^2.0.0-beta.20",
"@docusaurus/theme-common": "2.0.0-beta.20",
"@docusaurus/core": "^2.0.0-beta.21",
"@docusaurus/preset-classic": "^2.0.0-beta.21",
"@docusaurus/remark-plugin-npm2yarn": "^2.0.0-beta.21",
"@docusaurus/theme-common": "2.0.0-beta.21",
"@mdx-js/react": "1.6.22",
"classnames": "^2.3.1",
"mdx-mermaid": "^1.2.2",

View File

@@ -1,4 +1,23 @@
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "X-XSS-Protection",
"value": "1; mode=block"
}
]
}
],
"redirects": [
{
"source": "/schemas/models",

View File

@@ -6,7 +6,7 @@
Open Source. Full Stack. Own Your Data.
</p>
<!-- <p align="center" style="align: center;">
<img src="https://github.com/nextauthjs/adapters/actions/workflows/release.yml/badge.svg" alt="CI Test" />
<img src="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml/badge.svg?branch=main" alt="CI Test" />
<img src="https://img.shields.io/bundlephobia/minzip/@next-auth/prisma-adapter" alt="Bundle Size"/>
<img src="https://img.shields.io/npm/v/@next-auth/prisma-adapter" alt="@next-auth/prisma-adapter Version" />
</p> -->
@@ -150,7 +150,7 @@ type User
## Contributing
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/adapters/blob/main/CONTRIBUTING.md).
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/main/CONTRIBUTING.md).
## License

View File

@@ -3,9 +3,9 @@
"version": "1.0.3",
"description": "Dgraph adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/adapters",
"repository": "https://github.com/nextauthjs/next-auth",
"bugs": {
"url": "https://github.com/nextauthjs/adapters/issues"
"url": "https://github.com/nextauthjs/next-auth/issues"
},
"author": "Arnaud Derbey <arnaud@derbey.dev>",
"contributors": [],

View File

@@ -6,7 +6,7 @@
Open Source. Full Stack. Own Your Data.
</p>
<p align="center" style="align: center;">
<img src="https://github.com/nextauthjs/adapters/actions/workflows/release.yml/badge.svg" alt="Build Test" />
<img src="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml/badge.svg?branch=main" alt="Build Test" />
<img src="https://img.shields.io/bundlephobia/minzip/@next-auth/dynamodb-adapter/latest" alt="Bundle Size"/>
<img src="https://img.shields.io/npm/v/@next-auth/dynamodb-adapter" alt="@next-auth/dynamodb-adapter Version" />
</p>
@@ -96,7 +96,7 @@ Here is a schema of the table :
## Contributing
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/adapters/blob/main/CONTRIBUTING.md).
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/main/CONTRIBUTING.md).
## License

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/dynamodb-adapter",
"repository": "https://github.com/nextauthjs/adapters",
"repository": "https://github.com/nextauthjs/next-auth",
"version": "1.0.3",
"description": "AWS DynamoDB adapter for next-auth.",
"keywords": [

View File

@@ -7,7 +7,7 @@
Open Source. Full Stack. Own Your Data.
</p>
<p align="center" style="align: center;">
<img src="https://github.com/nextauthjs/adapters/actions/workflows/release.yml/badge.svg" alt="Build Test" />
<img src="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml/badge.svg?branch=main" alt="Build Test" />
<a href="https://www.npmjs.com/package/@next-auth/faunadb-adapter" target="_blank"><img src="https://img.shields.io/bundlephobia/minzip/@next-auth/fauna-adapter/next" alt="Bundle Size"/></a>
<a href="https://www.npmjs.com/package/@next-auth/faunadb-adapter" target="_blank"><img src="https://img.shields.io/npm/v/@next-auth/fauna-adapter/next" alt="@next-auth/fauna-adapter Version" /></a>
</p>
@@ -53,7 +53,7 @@ export default NextAuth({
## Contributing
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/adapters/blob/main/CONTRIBUTING.md).
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/main/CONTRIBUTING.md).
## License

View File

@@ -3,7 +3,7 @@
"version": "1.0.3",
"description": "Fauna Adapter for NextAuth",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/adapters",
"repository": "https://github.com/nextauthjs/next-auth",
"bugs": {
"url": "https://github.com/nextauthjs/next-auth/issues"
},

View File

@@ -7,7 +7,7 @@
Open Source. Full Stack. Own Your Data.
</p>
<p align="center" style="align: center;">
<img src="https://github.com/nextauthjs/adapters/actions/workflows/release.yml/badge.svg" alt="Build Test" />
<img src="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml/badge.svg?branch=main" alt="Build Test" />
<img src="https://img.shields.io/bundlephobia/minzip/@next-auth/firebase-adapter/latest" alt="Bundle Size"/>
<img src="https://img.shields.io/npm/v/@next-auth/firebase-adapter" alt="@next-auth/firebase-adapter Version" />
</p>
@@ -83,7 +83,7 @@ See [firebase.google.com/docs/web/setup](https://firebase.google.com/docs/web/se
## Contributing
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/adapters/blob/main/CONTRIBUTING.md).
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/main/CONTRIBUTING.md).
## License

View File

@@ -3,9 +3,9 @@
"version": "0.1.3",
"description": "Firebase adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/adapters",
"repository": "https://github.com/nextauthjs/next-auth",
"bugs": {
"url": "https://github.com/nextauthjs/adapters/issues"
"url": "https://github.com/nextauthjs/next-auth/issues"
},
"author": "Ron Houben <ron.houben85@gmail.com>",
"contributors": [

View File

@@ -6,7 +6,7 @@
Open Source. Full Stack. Own Your Data.
</p>
<p align="center" style="align: center;">
<img src="https://github.com/nextauthjs/adapters/actions/workflows/release.yml/badge.svg" alt="CI Test" />
<img src="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml/badge.svg?branch=main" alt="CI Test" />
<a href="https://www.npmjs.com/package/@next-auth/mikro-orm-adapter" target="_blank"><img src="https://img.shields.io/bundlephobia/minzip/@next-auth/mikro-orm-adapter/next" alt="Bundle Size"/></a>
<a href="https://www.npmjs.com/package/@next-auth/mikro-orm-adapter" target="_blank"><img src="https://img.shields.io/npm/v/@next-auth/mikro-orm-adapter/next" alt="@next-auth/mikro-orm-adapter Version" /></a>
</p>
@@ -49,7 +49,7 @@ This is the MikroORM Adapter for [`next-auth`](https://next-auth.js.org). This p
## Contributing
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/adapters/blob/main/CONTRIBUTING.md).
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/main/CONTRIBUTING.md).
## License

View File

@@ -3,7 +3,7 @@
"version": "2.0.1",
"description": "MikroORM adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/adapters",
"repository": "https://github.com/nextauthjs/next-auth",
"bugs": {
"url": "https://github.com/nextauthjs/next-auth/issues"
},

View File

@@ -6,7 +6,7 @@
Open Source. Full Stack. Own Your Data.
</p>
<p align="center" style="align: center;">
<img src="https://github.com/nextauthjs/adapters/actions/workflows/release.yml/badge.svg" alt="CI Test" />
<img src="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml/badge.svg?branch=main" alt="CI Test" />
<a href="https://www.npmjs.com/package/@next-auth/mongodb-adapter" target="_blank"><img src="https://img.shields.io/bundlephobia/minzip/@next-auth/mongodb-adapter" alt="Bundle Size"/></a>
<a href="https://www.npmjs.com/package/@next-auth/mongodb-adapter" target="_blank"><img src="https://img.shields.io/npm/v/@next-auth/mongodb-adapter" alt="@next-auth/mongodb-adapter Version" /></a>
</p>
@@ -79,7 +79,7 @@ export default NextAuth({
## Contributing
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/adapters/blob/main/CONTRIBUTING.md).
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/main/CONTRIBUTING.md).
## License

View File

@@ -3,7 +3,7 @@
"version": "1.0.3",
"description": "mongoDB adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/adapters",
"repository": "https://github.com/nextauthjs/next-auth",
"bugs": {
"url": "https://github.com/nextauthjs/next-auth/issues"
},

View File

@@ -6,7 +6,7 @@
Open Source. Full Stack. Own Your Data.
</p>
<p align="center" style="align: center;">
<img src="https://github.com/nextauthjs/adapters/actions/workflows/release.yml/badge.svg" alt="Canary CI Test" />
<img src="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml/badge.svg?branch=main" alt="Canary CI Test" />
<img src="https://img.shields.io/bundlephobia/minzip/@next-auth/neo4j-adapter" alt="Bundle Size"/>
<img src="https://img.shields.io/npm/v/@next-auth/neo4j-adapter" alt="@next-auth/neo4j-adapter Version" />
</p>
@@ -50,7 +50,7 @@ export default NextAuth({
## Contributing
We're open to all community contributions! If you'd like to contribute in any way, please first read our [Contributing Guide](https://github.com/nextauthjs/adapters/blob/canary/CONTRIBUTING.md).
We're open to all community contributions! If you'd like to contribute in any way, please first read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/canary/CONTRIBUTING.md).
## License

View File

@@ -3,7 +3,7 @@
"version": "1.0.3",
"description": "neo4j adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/adapters",
"repository": "https://github.com/nextauthjs/next-auth",
"bugs": {
"url": "https://github.com/nextauthjs/next-auth/issues"
},

View File

@@ -6,7 +6,7 @@
Open Source. Full Stack. Own Your Data.
</p>
<p align="center" style="align: center;">
<img src="https://github.com/nextauthjs/adapters/actions/workflows/release.yml/badge.svg" alt="CI Test" />
<img src="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml/badge.svg?branch=main" alt="CI Test" />
<img src="https://img.shields.io/bundlephobia/minzip/@next-auth/pouchdb-adapter" alt="Bundle Size"/>
<img src="https://img.shields.io/npm/v/@next-auth/pouchdb-adapter" alt="@next-auth/pouchdb-adapter Version" />
</p>
@@ -71,7 +71,7 @@ For more details, please see https://pouchdb.com/api.html#sync
## Contributing
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/adapters/blob/main/CONTRIBUTING.md).
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/main/CONTRIBUTING.md).
## License

View File

@@ -3,7 +3,7 @@
"version": "0.1.3",
"description": "PouchDB adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/adapters",
"repository": "https://github.com/nextauthjs/next-auth",
"bugs": {
"url": "https://github.com/nextauthjs/next-auth/issues"
},

View File

@@ -6,7 +6,7 @@
Open Source. Full Stack. Own Your Data.
</p>
<p align="center" style="align: center;">
<img src="https://github.com/nextauthjs/adapters/actions/workflows/release.yml/badge.svg" alt="CI Test" />
<img src="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml/badge.svg?branch=main" alt="CI Test" />
<a href="https://www.npmjs.com/package/@next-auth/prisma-adapter" target="_blank"><img src="https://img.shields.io/bundlephobia/minzip/@next-auth/prisma-adapter/next" alt="Bundle Size"/></a>
<a href="https://www.npmjs.com/package/@next-auth/prisma-adapter" target="_blank"><img src="https://img.shields.io/npm/v/@next-auth/prisma-adapter/next" alt="@next-auth/prisma-adapter Version" /></a>
</p>
@@ -48,7 +48,7 @@ export default NextAuth({
## Contributing
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/adapters/blob/main/CONTRIBUTING.md).
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/main/CONTRIBUTING.md).
## License

View File

@@ -3,7 +3,7 @@
"version": "1.0.3",
"description": "Prisma adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/adapters",
"repository": "https://github.com/nextauthjs/next-auth",
"bugs": {
"url": "https://github.com/nextauthjs/next-auth/issues"
},

View File

@@ -6,7 +6,7 @@
Open Source. Full Stack. Own Your Data.
</p>
<p align="center" style="align: center;">
<img src="https://github.com/nextauthjs/adapters/actions/workflows/release.yml/badge.svg" alt="CI Test" />
<img src="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml/badge.svg?branch=main" alt="CI Test" />
<img src="https://img.shields.io/bundlephobia/minzip/@next-auth/sequelize-adapter" alt="Bundle Size"/>
<img src="https://img.shields.io/npm/v/@next-auth/sequelize-adapter" alt="@next-auth/sequelize-adapter Version" />
</p>
@@ -89,7 +89,7 @@ export default NextAuth({
## Contributing
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/adapters/blob/main/CONTRIBUTING.md).
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/main/CONTRIBUTING.md).
## License

View File

@@ -3,9 +3,9 @@
"version": "1.0.4",
"description": "Sequelize adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/adapters",
"repository": "https://github.com/nextauthjs/next-auth",
"bugs": {
"url": "https://github.com/nextauthjs/adapters/issues"
"url": "https://github.com/nextauthjs/next-auth/issues"
},
"author": "github.com/luke-j",
"main": "dist/index.js",

View File

@@ -6,7 +6,7 @@
Open Source. Full Stack. Own Your Data.
</p>
<p align="center" style="align: center;">
<img src="https://github.com/nextauthjs/adapters/actions/workflows/release.yml/badge.svg" alt="Canary CI Test" />
<img src="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml/badge.svg?branch=main" alt="Canary CI Test" />
<img src="https://img.shields.io/bundlephobia/minzip/@next-auth/typeorm-legacy-adapter/canary" alt="Bundle Size"/>
<img src="https://img.shields.io/npm/v/@next-auth/typeorm-legacy-adapter" alt="@next-auth/typeorm-legacy-adapter Version" />
</p>

View File

@@ -3,9 +3,9 @@
"version": "1.0.3",
"description": "TypeORM (legacy) adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/adapters",
"repository": "https://github.com/nextauthjs/next-auth",
"bugs": {
"url": "https://github.com/nextauthjs/adapters/issues"
"url": "https://github.com/nextauthjs/next-auth/issues"
},
"author": "Iain Collins",
"contributors": [

View File

@@ -6,7 +6,7 @@
Open Source. Full Stack. Own Your Data.
</p>
<p align="center" style="align: center;">
<img src="https://github.com/nextauthjs/adapters/actions/workflows/release.yml/badge.svg" alt="CI Test" />
<img src="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml/badge.svg?branch=main" alt="CI Test" />
<img src="https://img.shields.io/bundlephobia/minzip/@next-auth/upstash-adapter" alt="Bundle Size"/>
<img src="https://img.shields.io/npm/v/@next-auth/upstash-adapter" alt="@next-auth/upstash-adapter Version" />
</p>
@@ -80,7 +80,7 @@ export default NextAuth({
## Contributing
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/adapters/blob/main/CONTRIBUTING.md).
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/main/CONTRIBUTING.md).
## License

View File

@@ -3,9 +3,9 @@
"version": "3.0.0",
"description": "Upstash adapter for next-auth. It uses Upstash's connectionless (HTTP based) Redis client.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/adapters",
"repository": "https://github.com/nextauthjs/next-auth",
"bugs": {
"url": "https://github.com/nextauthjs/adapters/issues"
"url": "https://github.com/nextauthjs/next-auth/issues"
},
"author": "github.com/kay-is",
"main": "dist/index.js",
@@ -36,6 +36,7 @@
"devDependencies": {
"@next-auth/adapter-test": "workspace:^0.0.0",
"@next-auth/tsconfig": "workspace:^0.0.0",
"@types/uuid": "^8.3.3",
"@upstash/redis": "^1.0.1",
"dotenv": "^10.0.0",
"jest": "^27.4.3",

View File

@@ -166,6 +166,10 @@ export default function App({
}
```
## Security
If you think you have found a vulnerability (or not sure) in NextAuth.js or any of the related packages (i.e. Adapters), we ask you to have a read of our [Security Policy](https://github.com/nextauthjs/next-auth/blob/main/SECURITY.md) to reach out responsibly. Please do not open Pull Requests/Issues/Discussions before consulting with us.
## Acknowledgments
[NextAuth.js is made possible thanks to all of its contributors.](https://next-auth.js.org/contributors)

View File

@@ -25,7 +25,6 @@ module.exports = (api) => {
ignore: [
"../src/**/__tests__/**",
"../src/adapters.ts",
"../src/core/types.ts",
"../src/providers/oauth-types.ts",
],
comments: false,

View File

@@ -1,6 +1,6 @@
{
"name": "next-auth",
"version": "4.5.0",
"version": "4.8.0",
"description": "Authentication for Next.js",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth.git",
@@ -62,16 +62,17 @@
"index.js",
"adapters.d.ts",
"middleware.d.ts",
"middleware.js"
"middleware.js",
"utils"
],
"license": "ISC",
"dependencies": {
"@babel/runtime": "^7.16.3",
"@panva/hkdf": "^1.0.1",
"@panva/oauth4webapi": "^0.0.10",
"cookie": "^0.4.1",
"jose": "^4.3.7",
"oauth": "^0.9.15",
"openid-client": "^5.1.0",
"preact": "^10.6.3",
"preact-render-to-string": "^5.1.19",
"uuid": "^8.3.2"
@@ -102,6 +103,7 @@
"@testing-library/react": "^13.3.0",
"@testing-library/react-hooks": "^8.0.0",
"@testing-library/user-event": "^14.2.0",
"@types/jest": "^28.1.3",
"@types/node": "^17.0.42",
"@types/nodemailer": "^6.4.4",
"@types/oauth": "^0.9.1",
@@ -114,13 +116,13 @@
"jest": "^28.1.1",
"jest-environment-jsdom": "^28.1.1",
"jest-watch-typeahead": "^1.1.0",
"msw": "^0.42.1",
"next": "^12.1.6",
"msw": "^0.42.3",
"next": "12.1.7-canary.51",
"postcss": "^8.4.14",
"postcss-cli": "^9.1.0",
"postcss-nested": "^5.0.6",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react": "^18",
"react-dom": "^18",
"whatwg-fetch": "^3.6.2"
},
"engines": {

View File

@@ -52,7 +52,7 @@ export interface VerificationToken {
* - `deleteUser`
* - `unlinkAccount`
*
* [Community adapters](https://github.com/nextauthjs/adapters) |
* [Adapters Overview](https://next-auth.js.org/adapters/overview) |
* [Create a custom adapter](https://next-auth.js.org/tutorials/creating-a-database-adapter)
*/
export interface Adapter {

View File

@@ -9,12 +9,13 @@ import { SessionStore } from "./lib/cookie"
import type { NextAuthAction, NextAuthOptions } from "./types"
import type { Cookie } from "./lib/cookie"
import type { ErrorType } from "./pages/error"
import { parse as parseCookie } from "cookie"
export interface RequestInternal {
/** @default "http://localhost:3000" */
host?: string
method?: string
cookies?: Record<string, string>
cookies?: Partial<Record<string, string>>
headers?: Record<string, any>
query?: Record<string, any>
body?: Record<string, any>
@@ -68,7 +69,7 @@ async function toInternalRequest(
method: req.method,
headers,
body: await getBody(req),
cookies: {},
cookies: parseCookie(req.headers.get("cookie") ?? ""),
providerId: nextauth[1],
error: url.searchParams.get("error") ?? nextauth[1],
host: detectHost(headers["x-forwarded-host"] ?? headers.host),

View File

@@ -44,7 +44,7 @@ export function assertConfig(
): ConfigError | WarningCode | undefined {
const { options, req } = params
// req.query isn't defined when asserting `getServerSession` for example
// req.query isn't defined when asserting `unstable_getServerSession` for example
if (!req.query?.nextauth && !req.action) {
return new MissingAPIRoute(
"Cannot find [...nextauth].{js,ts} in `/pages/api/auth`. Make sure the filename is written correctly."

View File

@@ -155,7 +155,7 @@ export default async function callbackHandler(params: {
// 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 })
await events.linkAccount?.({ user, account, profile })
// As they are already signed in, we don't need to do anything after linking them
return { session, user, isNewUser }
@@ -205,7 +205,7 @@ export default async function callbackHandler(params: {
await events.createUser?.({ user })
await linkAccount({ ...account, userId: user.id })
await events.linkAccount?.({ user, account })
await events.linkAccount?.({ user, account, profile })
session = useJwtSession
? {}

View File

@@ -120,7 +120,7 @@ export class SessionStore {
constructor(
option: CookieOption,
req: {
cookies?: Record<string, string>
cookies?: Partial<Record<string, string> | Map<string, string>>
headers?: Headers | IncomingHttpHeaders | Record<string, string>
},
logger: LoggerInstance | Console
@@ -128,11 +128,16 @@ export class SessionStore {
this.#logger = logger
this.#option = option
if (!req) return
const { cookies } = req
const { name: cookieName } = option
for (const name in req.cookies) {
if (name.startsWith(option.name)) {
this.#chunks[name] = req.cookies[name]
if (cookies instanceof Map) {
for (const name of cookies.keys()) {
if (name.startsWith(cookieName)) this.#chunks[name] = cookies.get(name)
}
} else {
for (const name in cookies) {
if (name.startsWith(cookieName)) this.#chunks[name] = cookies[name]
}
}
}

View File

@@ -1,32 +0,0 @@
import { discoveryRequest, processDiscoveryResponse } from "@panva/oauth4webapi"
import type { AuthorizationServer } from "@panva/oauth4webapi"
import type { InternalProvider } from "src/lib/types"
export default async function getAuthorizationServer(
provider: InternalProvider<"oauth">
): Promise<AuthorizationServer> {
if (provider.idToken) {
const issuer = new URL(provider.issuer as string)
return await discoveryRequest(issuer).then(
async (response) => await processDiscoveryResponse(issuer, response)
)
} else {
return {
issuer: provider.issuer as string,
authorization_endpoint:
typeof provider.authorization === "string"
? provider.authorization
: provider.authorization?.url,
token_endpoint:
typeof provider.token === "string"
? provider.token
: provider.token?.url,
userinfo_endpoint:
typeof provider.userinfo === "string"
? provider.userinfo
: provider.userinfo?.url,
jwks_uri: provider.jwks_uri,
}
}
}

View File

@@ -1,7 +1,7 @@
import { openidClient } from "./client"
import { oAuth1Client } from "./client-legacy"
import { createState } from "./state-handler"
import { createPKCE } from "./pkce-handler"
import getAuthorizationServer from "./authorization-server"
import type { AuthorizationParameters } from "openid-client"
import type { InternalOptions } from "../../types"
@@ -50,53 +50,28 @@ export default async function getAuthorizationUrl(params: {
return { redirect: url }
}
const authorizationServer = await getAuthorizationServer(provider)
if (!authorizationServer.authorization_endpoint) throw new Error()
const authorizationUrl = new URL(authorizationServer.authorization_endpoint)
for (const [key, value] of Object.entries(params)) {
authorizationUrl.searchParams.set(key, value as string)
}
authorizationUrl.searchParams.set("client_id", provider.clientId as string)
authorizationUrl.searchParams.set("redirect_uri", provider.callbackUrl)
if (typeof provider.authorization !== "string" && provider.authorization) {
const { params: authorizationEndpointParams } = provider.authorization
if (typeof authorizationEndpointParams?.response_type === "string") {
authorizationUrl.searchParams.set(
"response_type",
authorizationEndpointParams.response_type
)
}
if (typeof authorizationEndpointParams?.scope === "string") {
authorizationUrl.searchParams.set(
"scope",
authorizationEndpointParams.scope
)
}
}
const client = await openidClient(options)
const authorizationParams: AuthorizationParameters = params
const cookies: Cookie[] = []
const pkce = await createPKCE(options, authorizationServer)
authorizationUrl.searchParams.set("code_challenge", pkce.code_challenge)
authorizationUrl.searchParams.set(
"code_challenge_method",
pkce.code_challenge_method
)
cookies.push(pkce.cookie)
const state = await createState(options)
if (!pkce.isSupported && state) {
authorizationUrl.searchParams.set("state", state.value)
if (state) {
authorizationParams.state = state.value
cookies.push(state.cookie)
}
logger.debug("GET_AUTHORIZATION_URL", { authorizationUrl, cookies })
return { redirect: authorizationUrl.href, cookies }
const pkce = await createPKCE(options)
if (pkce) {
authorizationParams.code_challenge = pkce.code_challenge
authorizationParams.code_challenge_method = pkce.code_challenge_method
cookies.push(pkce.cookie)
}
const url = client.authorizationUrl(authorizationParams)
logger.debug("GET_AUTHORIZATION_URL", { url, cookies })
return { redirect: url, cookies }
} catch (error) {
logger.error("GET_AUTHORIZATION_URL_ERROR", error as Error)
throw error

View File

@@ -1,31 +1,16 @@
import { TokenSet } from "openid-client"
import { openidClient } from "./client"
import { oAuth1Client } from "./client-legacy"
import { useState } from "./state-handler"
import { usePKCECodeVerifier } from "./pkce-handler"
import { OAuthCallbackError } from "../../errors"
import {
authorizationCodeGrantRequest,
expectNoState,
getValidatedIdTokenClaims,
isOAuth2Error,
processAuthorizationCodeOAuth2Response,
processAuthorizationCodeOpenIDResponse,
processUserInfoResponse,
userInfoRequest,
validateAuthResponse,
} from "@panva/oauth4webapi"
import getAuthorizationServer from "./authorization-server"
import type { CallbackParamsType } from "openid-client"
import type { Account, LoggerInstance, Profile } from "../../.."
import type { OAuthChecks, OAuthConfig } from "../../../providers"
import type { InternalOptions } from "../../types"
import type { RequestInternal, OutgoingResponse } from "../.."
import type { Cookie } from "../cookie"
import type {
OAuth2Error,
OAuth2TokenEndpointResponse,
OpenIDTokenEndpointResponse,
} from "@panva/oauth4webapi"
export default async function oAuthCallback(params: {
options: InternalOptions<"oauth">
@@ -34,7 +19,7 @@ export default async function oAuthCallback(params: {
method: Required<RequestInternal>["method"]
cookies: RequestInternal["cookies"]
}): Promise<GetProfileResult & { cookies?: OutgoingResponse["cookies"] }> {
const { options, query, body, cookies } = params
const { options, query, body, method, cookies } = params
const { logger, provider } = options
const errorMessage = body?.error ?? query?.error
@@ -80,111 +65,81 @@ export default async function oAuthCallback(params: {
}
try {
const client = openidClient(provider)
const authorizationServer = await getAuthorizationServer(provider)
const client = await openidClient(options)
let tokens:
| OpenIDTokenEndpointResponse
| OAuth2TokenEndpointResponse
| OAuth2Error
let tokens: TokenSet
let expectedState: string | typeof expectNoState = expectNoState
const checks: OAuthChecks = {}
const resCookies: Cookie[] = []
const authParams = new URLSearchParams(query)
const codeVerifier = cookies?.[options.cookies.pkceCodeVerifier.name]
const pkce = await usePKCECodeVerifier(
codeVerifier,
options,
authorizationServer
)
resCookies.push(pkce.cookie)
const state = await useState(cookies?.[options.cookies.state.name], options)
if (!pkce.isSupported && state) {
if (state) {
checks.state = state.value
resCookies.push(state.cookie)
expectedState = state.value
authParams.append("state", state.value)
}
const callbackParameters = validateAuthResponse(
authorizationServer,
client,
authParams,
expectedState
)
if (isOAuth2Error(callbackParameters)) {
throw new OAuthCallbackError(callbackParameters.error)
const codeVerifier = cookies?.[options.cookies.pkceCodeVerifier.name]
const pkce = await usePKCECodeVerifier(codeVerifier, options)
if (pkce) {
checks.code_verifier = pkce.codeVerifier
resCookies.push(pkce.cookie)
}
const response = await authorizationCodeGrantRequest(
authorizationServer,
client,
callbackParameters,
provider.callbackUrl,
pkce.codeVerifier
)
const params: CallbackParamsType = {
...client.callbackParams({
url: `http://n?${new URLSearchParams(query)}`,
// TODO: Ask to allow object to be passed upstream:
// https://github.com/panva/node-openid-client/blob/3ae206dfc78c02134aa87a07f693052c637cab84/types/index.d.ts#L439
// @ts-expect-error
body,
method,
}),
// @ts-expect-error
...provider.token?.params,
}
if (typeof provider.token !== "string" && provider.token?.request) {
const params = {
...callbackParameters,
...provider.token?.params,
}
const checks = new URLSearchParams()
if (state) checks.append("state", state.value)
checks.append("code_verifier", pkce.codeVerifier)
// @ts-expect-error
if (provider.token?.request) {
// @ts-expect-error
const response = await provider.token.request({
provider,
params,
checks,
client,
})
tokens = response.tokens
tokens = new TokenSet(response.tokens)
} else if (provider.idToken) {
tokens = await processAuthorizationCodeOpenIDResponse(
authorizationServer,
client,
response
)
tokens = await client.callback(provider.callbackUrl, params, checks)
} else {
tokens = await processAuthorizationCodeOAuth2Response(
authorizationServer,
client,
response
)
tokens = await client.oauthCallback(provider.callbackUrl, params, checks)
}
if (isOAuth2Error(tokens)) {
throw new OAuthCallbackError(tokens.error)
// REVIEW: How can scope be returned as an array?
if (Array.isArray(tokens.scope)) {
tokens.scope = tokens.scope.join(" ")
}
let profile: Profile | Response
if (typeof provider.userinfo !== "string" && provider.userinfo?.request) {
let profile: Profile
// @ts-expect-error
if (provider.userinfo?.request) {
// @ts-expect-error
profile = await provider.userinfo.request({
provider,
tokens,
client,
})
} else if (provider.idToken) {
const idToken = getValidatedIdTokenClaims(tokens)
profile = await processUserInfoResponse(
authorizationServer,
client,
idToken?.sub as string,
response
)
profile = tokens.claims()
} else {
profile = await userInfoRequest(
authorizationServer,
client,
tokens.access_token
)
profile = await client.userinfo(tokens, {
// @ts-expect-error
params: provider.userinfo?.params,
})
}
const profileResult = await getProfile({
profile: profile as Profile,
profile,
provider,
tokens,
logger,
@@ -201,7 +156,7 @@ export default async function oAuthCallback(params: {
export interface GetProfileParams {
profile: Profile
tokens: OpenIDTokenEndpointResponse | OAuth2TokenEndpointResponse
tokens: TokenSet
provider: OAuthConfig<any>
logger: LoggerInstance
}

View File

@@ -1,20 +1,14 @@
import { InternalProvider } from "src/lib/types"
import type { Client as WebApiClient } from "@panva/oauth4webapi"
import { Issuer, custom } from "openid-client"
import type { Client } from "openid-client"
import type { InternalOptions } from "../../types"
/**
* NOTE: We can add auto discovery of the provider's endpoint
* that requires only one endpoint to be specified by the user.
* Check out `Issuer.discover`
*
* Client supporting OAuth 2.x and OIDC
*/
export function webApiClient(provider: InternalProvider<"oauth">): WebApiClient {
return {
client_id: provider.clientId as string,
client_secret: provider.clientSecret as string,
token_endpoint_auth_method: "client_secret_basic",
...provider.client,
}
}
export async function openidClient(
options: InternalOptions<"oauth">
): Promise<Client> {
@@ -37,4 +31,21 @@ export async function openidClient(
userinfo_endpoint: provider.userinfo?.url ?? provider.userinfo,
})
}
const client = new issuer.Client(
{
client_id: provider.clientId as string,
client_secret: provider.clientSecret as string,
redirect_uris: [provider.callbackUrl],
...provider.client,
},
provider.jwks
)
// allow a 10 second skew
// See https://github.com/nextauthjs/next-auth/issues/3032
// and https://github.com/nextauthjs/next-auth/issues/3067
client[custom.clock_tolerance] = 10
return client
}

View File

@@ -1,9 +0,0 @@
import { generateRandomCodeVerifier } from "@panva/oauth4webapi"
/**
* Generate random `state` value encoded as base64url. This method returns oauth4webapi's `generateRandomCodeVerifier` for convenience.
* @see {@link https://github.com/panva/oauth4webapi/blob/main/docs/functions/generateRandomCodeVerifier.md generateRandomCodeVerifier.}
*/
export function generateRandomState() {
return generateRandomCodeVerifier()
}

View File

@@ -1,43 +1,30 @@
import * as jwt from "../../../jwt"
import {
generateRandomCodeVerifier,
calculatePKCECodeChallenge,
} from "@panva/oauth4webapi"
import { generators } from "openid-client"
import type { InternalOptions } from "../../types"
import type { Cookie } from "../cookie"
import type { AuthorizationServer } from "@panva/oauth4webapi"
const PKCE_CODE_CHALLENGE_METHOD = "S256"
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
/**
* Check if PKCE is supported by the authorization server.
* @see {@link https://datatracker.ietf.org/doc/html/rfc8414#section-2 code_challenge_methods_supported}
* @param as The authorization server. @see {@link https://github.com/panva/oauth4webapi/blob/main/docs/interfaces/AuthorizationServer.md AuthorizationServer}
*/
function isPKCESupported(as: AuthorizationServer) {
return !!as.code_challenge_methods_supported?.includes(
PKCE_CODE_CHALLENGE_METHOD
)
}
/**
* Returns `code_challenge` and `code_challenge_method`
* and saves them in a cookie.
*/
export async function createPKCE(
options: InternalOptions<"oauth">,
as: AuthorizationServer
): Promise<{
code_challenge: string
code_challenge_method: "S256"
cookie: Cookie
isSupported: boolean
}> {
const { cookies, logger } = options
const code_verifier = generateRandomCodeVerifier()
const code_challenge = await calculatePKCECodeChallenge(code_verifier)
export async function createPKCE(options: InternalOptions<"oauth">): Promise<
| undefined
| {
code_challenge: string
code_challenge_method: "S256"
cookie: Cookie
}
> {
const { cookies, logger, provider } = options
if (!provider.checks?.includes("pkce")) {
// Provider does not support PKCE, return nothing.
return
}
const code_verifier = generators.codeVerifier()
const code_challenge = generators.codeChallenge(code_verifier)
const expires = new Date()
expires.setTime(expires.getTime() + PKCE_MAX_AGE * 1000)
@@ -64,37 +51,34 @@ export async function createPKCE(
value: encryptedCodeVerifier,
options: { ...cookies.pkceCodeVerifier.options, expires },
},
isSupported: isPKCESupported(as),
}
}
/**
* Returns `code_verifier`,
* Returns code_verifier if provider uses PKCE,
* and clears the container cookie afterwards.
*/
export async function usePKCECodeVerifier(
codeVerifier: string | undefined,
options: InternalOptions<"oauth">,
as: AuthorizationServer
): Promise<{ codeVerifier: string; cookie: Cookie; isSupported: boolean }> {
if (codeVerifier === undefined) throw new Error("Invalid code verifier")
options: InternalOptions<"oauth">
): Promise<{ codeVerifier: string; cookie: Cookie } | undefined> {
const { cookies, provider } = options
const { cookies } = options
if (!provider?.checks?.includes("pkce") || !codeVerifier) {
return
}
const pkce = await jwt.decode({
const pkce = (await jwt.decode({
...options.jwt,
token: codeVerifier,
})
if (pkce === null) throw new Error("Invalid code verifier")
})) as any
return {
codeVerifier: pkce.code_verifier as string,
codeVerifier: pkce?.code_verifier ?? undefined,
cookie: {
name: cookies.pkceCodeVerifier.name,
value: "",
options: { ...cookies.pkceCodeVerifier.options, maxAge: 0 },
},
isSupported: isPKCESupported(as),
}
}

View File

@@ -2,7 +2,6 @@ import { generators } from "openid-client"
import type { InternalOptions } from "../../types"
import type { Cookie } from "../cookie"
import { generateRandomState } from "./helper"
const STATE_MAX_AGE = 60 * 15 // 15 minutes in seconds
@@ -17,7 +16,7 @@ export async function createState(
return
}
const state = generateRandomState()
const state = generators.state()
const encodedState = await jwt.encode({
...jwt,

View File

@@ -30,12 +30,25 @@ export default async function signin(params: {
return { redirect: `${url}/error?error=OAuthSignin` }
}
} else if (provider.type === "email") {
// Note: Technically the part of the email address local mailbox element
// (everything before the @ symbol) should be treated as 'case sensitive'
// according to RFC 2821, but in practice this causes more problems than
// it solves. We treat email addresses as all lower case. If anyone
// complains about this we can make strict RFC 2821 compliance an option.
const email = body?.email?.toLowerCase() ?? null
/**
* @note Technically the part of the email address local mailbox element
* (everything before the @ symbol) should be treated as 'case sensitive'
* according to RFC 2821, but in practice this causes more problems than
* it solves. We treat email addresses as all lower case. If anyone
* complains about this we can make strict RFC 2821 compliance an option.
*/
let email = body?.email?.toLowerCase()
if (!email) return { redirect: `${url}/error?error=EmailSignin` }
email = email
.split(",")[0]
.trim()
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#x27;")
// Verified in `assertConfig`
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion

View File

@@ -11,11 +11,10 @@ import type { TokenSetParameters } from "openid-client"
import type { JWT, JWTOptions } from "../jwt"
import type { LoggerInstance } from "../utils/logger"
import type { CookieSerializeOptions } from "cookie"
import type { TokenEndpointResponse } from "@panva/oauth4webapi"
import type { NextApiRequest, NextApiResponse } from "next"
import { InternalUrl } from "../utils/parse-url"
import type { InternalUrl } from "../utils/parse-url"
export type Awaitable<T> = T | PromiseLike<T>
@@ -118,7 +117,7 @@ export interface NextAuthOptions {
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#adapter) |
* [Community adapters](https://github.com/nextauthjs/adapters)
* [Adapters Overview](https://next-auth.js.org/adapters/overview)
*/
adapter?: Adapter
/**
@@ -225,7 +224,7 @@ export interface Theme {
* Some of them are available with different casing,
* but they refer to the same value.
*/
export type TokenSet = TokenEndpointResponse
export type TokenSet = TokenSetParameters
/**
* Usually contains information about the provider being used
@@ -390,7 +389,11 @@ export interface EventCallbacks {
signOut: (message: { session: Session; token: JWT }) => Awaitable<void>
createUser: (message: { user: User }) => Awaitable<void>
updateUser: (message: { user: User }) => Awaitable<void>
linkAccount: (message: { user: User; account: Account }) => Awaitable<void>
linkAccount: (message: {
user: User
account: Account
profile: User
}) => Awaitable<void>
/**
* The message object will contain one of these depending on
* if you use JWT or database persisted sessions:

View File

@@ -67,7 +67,7 @@ function NextAuth(
options: NextAuthOptions
): any
/** Tha main entry point to next-auth */
/** The main entry point to next-auth */
function NextAuth(
...args:
| [NextAuthOptions]
@@ -83,26 +83,36 @@ function NextAuth(
export default NextAuth
export async function getServerSession(
context:
| GetServerSidePropsContext
| { req: NextApiRequest; res: NextApiResponse },
options: NextAuthOptions
export async function unstable_getServerSession(
...args:
| [GetServerSidePropsContext['req'], GetServerSidePropsContext['res'], NextAuthOptions]
| [NextApiRequest, NextApiResponse, NextAuthOptions]
): Promise<Session | null> {
console.warn(
"[next-auth][warn][EXPERIMENTAL_API]",
"\n`unstable_getServerSession` is experimental and may be removed or changed in the future, as the name suggested.",
`\nhttps://next-auth.js.org/configuration/nextjs#unstable_getServerSession}`,
`\nhttps://next-auth.js.org/warnings#EXPERIMENTAL_API`
)
const [req, res, options] = args;
options.secret = options.secret ?? process.env.NEXTAUTH_SECRET
const session = await NextAuthHandler<Session | {}>({
options,
req: {
host: detectHost(context.req.headers["x-forwarded-host"]),
host: detectHost(req.headers["x-forwarded-host"]),
action: "session",
method: "GET",
cookies: context.req.cookies,
headers: context.req.headers,
cookies: req.cookies,
headers: req.headers,
},
})
const { body, cookies } = session
cookies?.forEach((cookie) => setCookie(context.res, cookie))
cookies?.forEach((cookie) => setCookie(res, cookie))
if (body && Object.keys(body).length) return body as Session
return null

View File

@@ -66,7 +66,7 @@ export interface NextAuthMiddlewareOptions {
* @example
*
* ```js
* // `pages/admin/_middleware.js`
* // `middleware.js`
* import { withAuth } from "next-auth/middleware"
*
* export default withAuth({
@@ -74,6 +74,9 @@ export interface NextAuthMiddlewareOptions {
* authorized: ({ token }) => token?.user.isAdmin
* }
* })
*
* export const config = { matcher: ["/admin"] }
*
* ```
*
* ---
@@ -149,7 +152,7 @@ export type WithAuthArgs =
* @example
*
* ```js
* // `pages/_middleware.js`
* // `middleware.js`
* export { default } from "next-auth/middleware"
* ```
*

View File

@@ -1,4 +1,3 @@
import { AuthorizationServer, userInfoRequest } from "@panva/oauth4webapi"
import type { OAuthConfig, OAuthUserConfig } from "."
interface FacebookPictureData {
@@ -27,17 +26,11 @@ export default function Facebook<P extends FacebookProfile>(
// https://developers.facebook.com/docs/graph-api/reference/user/#fields
params: { fields: "id,name,email,picture" },
async request({ tokens, client, provider }) {
// @ts-expect-error
const userinfo_endpoint = new URL(provider.userinfo?.url)
// @ts-expect-error
Object.entries(provider.userinfo?.params).forEach(([key, value]) => {
userinfo_endpoint.searchParams.append(key, value as string)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return await client.userinfo(tokens.access_token!, {
// @ts-expect-error
params: provider.userinfo?.params,
})
const as: AuthorizationServer = {
issuer: options.issuer as string,
userinfo_endpoint: userinfo_endpoint.href,
}
return await userInfoRequest(as, client, tokens.access_token)
},
},
profile(profile: P) {

View File

@@ -1,20 +1,33 @@
import type { CommonProviderOptions } from "../providers"
import type { Profile, TokenSet, User, Awaitable } from ".."
import type {
AuthorizationParameters,
CallbackParamsType,
Issuer,
ClientMetadata,
IssuerMetadata,
OAuthCallbackChecks,
OpenIDCallbackChecks,
HttpOptions,
} from "openid-client"
import type { JWK } from "jose"
import type { AuthorizationServer, Client } from "@panva/oauth4webapi"
type Client = InstanceType<Issuer["Client"]>
export type { OAuthProviderType } from "./oauth-types"
type ChecksType = "pkce" | "state" | "none"
type PartialIssuer = Partial<Pick<AuthorizationServer, "jwks_uri" | "issuer">>
export type OAuthChecks = OpenIDCallbackChecks | OAuthCallbackChecks
type PartialIssuer = Partial<Pick<IssuerMetadata, "jwks_endpoint" | "issuer">>
type UrlParams = Record<string, unknown>
type EndpointRequest<C, R, P> = (
context: C & {
/** `oauth4webapi` Client */
/** `openid-client` Client */
client: Client
/** Provider is passed for convenience, ans also contains the `callbackUrl`. */
provider: OAuthConfig<P> & {
@@ -48,7 +61,8 @@ export type EndpointHandler<
R = any
> = AdvancedEndpointHandler<P, C, R>
export type AuthorizationEndpointHandler = EndpointHandler<UrlParams>
export type AuthorizationEndpointHandler =
EndpointHandler<AuthorizationParameters>
export type TokenEndpointHandler = EndpointHandler<
UrlParams,
@@ -57,12 +71,12 @@ export type TokenEndpointHandler = EndpointHandler<
* Parameters extracted from the request to the `/api/auth/callback/:providerId` endpoint.
* Contains params like `state`.
*/
params: URLSearchParams
params: CallbackParamsType
/**
* When using this custom flow, make sure to do all the necessary security checks.
* This object contains parameters you have to match against the request to make sure it is valid.
* Thist object contains parameters you have to match against the request to make sure it is valid.
*/
checks: URLSearchParams
checks: OAuthChecks
},
{
tokens: TokenSet
@@ -72,7 +86,7 @@ export type TokenEndpointHandler = EndpointHandler<
export type UserinfoEndpointHandler = EndpointHandler<
UrlParams,
{ tokens: TokenSet },
Profile | Response
Profile
>
export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
@@ -98,7 +112,7 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
version?: string
profile?: (profile: P, tokens: TokenSet) => Awaitable<User & { id: string }>
checks?: ChecksType | ChecksType[]
client?: Partial<Client>
client?: Partial<ClientMetadata>
jwks?: { keys: JWK[] }
clientId?: string
clientSecret?: string
@@ -114,6 +128,11 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
idToken?: boolean
// TODO: only allow for BattleNet
region?: string
// TODO: only allow for some
issuer?: string
/** Read more at: https://github.com/panva/node-openid-client/tree/main/docs#customizing-http-requests */
httpOptions?: HttpOptions
/**
* The options provided by the user.
* We will perform a deep-merge of these values

View File

@@ -1,10 +1,4 @@
import type { OAuthConfig, OAuthUserConfig } from "."
import {
authorizationCodeGrantRequest,
AuthorizationServer,
isOAuth2Error,
processAuthorizationCodeOAuth2Response,
} from "@panva/oauth4webapi"
export interface TwitterLegacyProfile {
id: number
@@ -189,33 +183,13 @@ export default function Twitter<
url: "https://api.twitter.com/2/oauth2/token",
// TODO: Remove this
async request({ client, params, checks, provider }) {
const as: AuthorizationServer = {
issuer: options.issuer as string,
// @ts-expect-error
token_endpoint: provider.token?.url,
}
const additionalParameters = new URLSearchParams()
additionalParameters.append("client_id", options.clientId)
const response = await authorizationCodeGrantRequest(
as,
client,
params,
const response = await client.oauthCallback(
provider.callbackUrl,
checks.get("code_verifier") as string,
{
additionalParameters,
}
params,
checks,
{ exchangeBody: { client_id: options.clientId } }
)
const tokens = await processAuthorizationCodeOAuth2Response(
as,
client,
response
)
if (isOAuth2Error(tokens)) {
throw new Error()
}
return { tokens }
return { tokens: response }
},
},
userinfo: {

View File

@@ -175,7 +175,7 @@ export async function getProviders() {
export async function signIn<
P extends RedirectableProviderType | undefined = undefined
>(
provider?: LiteralUnion<BuiltInProviderType>,
provider?: LiteralUnion<P extends RedirectableProviderType ? P | BuiltInProviderType : BuiltInProviderType>,
options?: SignInOptions,
authorizationParams?: SignInAuthorizationParams
): Promise<

View File

@@ -1,3 +1,4 @@
import { InvalidCallbackUrl, MissingSecret } from "../src/core/errors"
import { handler } from "./lib"
it("Show error page if secret is not defined", async () => {
@@ -10,7 +11,23 @@ it("Show error page if secret is not defined", async () => {
expect(res.html).toMatch(/there is a problem with the server configuration./i)
expect(res.html).toMatch(/check the server logs for more information./i)
expect(log.error).toBeCalledWith("NO_SECRET", expect.anything())
expect(log.error).toBeCalledWith("NO_SECRET", expect.any(MissingSecret))
})
it("Should show configuration error page on invalid `callbackUrl`", async () => {
const { res, log } = await handler(
{ providers: [] },
{ prod: true, params: { callbackUrl: "invalid-callback" } }
)
expect(res.status).toBe(500)
expect(res.html).toMatch(/there is a problem with the server configuration./i)
expect(res.html).toMatch(/check the server logs for more information./i)
expect(log.error).toBeCalledWith(
"INVALID_CALLBACK_URL_ERROR",
expect.any(InvalidCallbackUrl)
)
})
it("Allow relative `callbackUrl`", async () => {
@@ -22,6 +39,6 @@ it("Allow relative `callbackUrl`", async () => {
expect(res.status).not.toBe(500)
expect(log.error).not.toBeCalledWith(
"INVALID_CALLBACK_URL_ERROR",
expect.anything()
expect.any(InvalidCallbackUrl)
)
})

View File

@@ -0,0 +1,59 @@
import { createCSRF, handler } from "./lib"
import EmailProvider from "../src/providers/email"
const originalEmail = "balazs@email.com"
test.each([
[originalEmail, `,<a href="example.com">Click here!</a>`],
[originalEmail, ""],
])("Sanitize email", async (emailOriginal, emailCompromised) => {
const sendEmail = jest.fn()
const { secret, csrf } = createCSRF()
const email = {
original: emailOriginal,
compromised: `${emailOriginal}${emailCompromised}`,
}
const { res } = await handler(
{
providers: [EmailProvider({ sendVerificationRequest: sendEmail })],
adapter: {
getUserByEmail: (email) => ({ id: "1", email, emailVerified: null }),
createVerificationToken: (token) => token,
} as any,
secret,
},
{
prod: true,
path: "signin/email",
requestInit: {
method: "POST",
body: JSON.stringify({
email: email.compromised,
csrfToken: csrf.value,
}),
headers: { "Content-Type": "application/json", Cookie: csrf.cookie },
},
}
)
if (!emailCompromised) {
expect(res.redirect).toBe(
"http://localhost:3000/api/auth/verify-request?provider=email&type=email"
)
expect(sendEmail).toHaveBeenCalledWith(
expect.objectContaining({
identifier: email.original,
token: expect.any(String),
})
)
} else {
expect(res.redirect).not.toContain("error=EmailSignin")
const emailTo = sendEmail.mock.calls[0][0].identifier
expect(emailTo).not.toBe(email.compromised)
expect(emailTo).toBe(email.original)
}
})

View File

@@ -0,0 +1,53 @@
import type { NextApiRequest } from "next"
import { MissingSecret } from "../src/core/errors"
import { unstable_getServerSession } from "../src/next"
import { mockLogger } from "./lib"
let originalWarn = console.warn
let logger = mockLogger()
beforeEach(() => {
process.env.NODE_ENV = "production"
process.env.NEXTAUTH_URL = "http://localhost"
console.warn = jest.fn()
})
afterEach(() => {
logger = mockLogger()
process.env.NODE_ENV = "test"
delete process.env.NEXTAUTH_URL
console.warn = originalWarn
})
describe("Treat secret correctly", () => {
const req: any = { headers: {} }
const res: any = { setHeader: jest.fn(), getHeader: jest.fn() }
it("Read from NEXTAUTH_SECRET", async () => {
process.env.NEXTAUTH_SECRET = "secret"
await unstable_getServerSession(req, res, { providers: [], logger })
expect(logger.error).toBeCalledTimes(0)
expect(logger.error).not.toBeCalledWith("NO_SECRET")
delete process.env.NEXTAUTH_SECRET
})
it("Read from options.secret", async () => {
await unstable_getServerSession(req, res, {
providers: [],
logger,
secret: "secret",
})
expect(logger.error).toBeCalledTimes(0)
expect(logger.error).not.toBeCalledWith("NO_SECRET")
})
it("Error if missing NEXTAUTH_SECRET and secret", async () => {
await unstable_getServerSession(req, res, { providers: [], logger })
expect(logger.error).toBeCalledTimes(1)
expect(logger.error).toBeCalledWith("NO_SECRET", expect.any(MissingSecret))
})
})

View File

@@ -1,39 +1,37 @@
import { createHash } from "crypto"
import type { LoggerInstance, NextAuthOptions } from "../src"
import { NextAuthHandler } from "../src/core"
export const mockLogger: () => LoggerInstance = () => ({
error: jest.fn(() => {}),
warn: jest.fn(() => {}),
debug: jest.fn(() => {}),
})
interface HandlerOptions {
prod?: boolean
path?: string
params?: URLSearchParams | Record<string, string>
requestInit?: RequestInit
}
export async function handler(
options: NextAuthOptions,
{
prod,
path,
params,
}: {
prod?: boolean
path?: string
params?: URLSearchParams | Record<string, string>
}
{ prod, path, params, requestInit }: HandlerOptions
) {
// @ts-ignore
if (prod) process.env.NODE_ENV = "production"
const mockLogger: LoggerInstance = {
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
}
const url = new URL(
`http://localhost/api/auth/${path ?? "signin"}?${new URLSearchParams(
params ?? {}
)}`
)
const req = new Request(url, {
headers: {
host: "",
},
})
const req = new Request(url, { headers: { host: "" }, ...requestInit })
const logger = mockLogger()
const response = await NextAuthHandler({
req,
options: { secret: "secret", ...options, logger: mockLogger },
options: { secret: "secret", ...options, logger },
})
// @ts-ignore
if (prod) process.env.NODE_ENV = "test"
@@ -44,6 +42,17 @@ export async function handler(
html:
response.headers?.[0].value === "text/html" ? response.body : undefined,
},
log: mockLogger,
log: logger,
}
}
export function createCSRF() {
const secret = "secret"
const value = "csrf"
const token = createHash("sha256").update(`${value}${secret}`).digest("hex")
return {
secret,
csrf: { value, token, cookie: `next-auth.csrf-token=${value}|${token}` },
}
}

View File

@@ -14,7 +14,8 @@
"stripInternal": true,
"skipDefaultLibCheck": true,
"baseUrl": ".",
"outDir": "."
"outDir": ".",
},
"exclude": ["./*.js", "./*.d.ts", "config", "**/__tests__", "tests"]
}

1903
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ export async function verify() {
if (!process.env.NPM_TOKEN_ORG) {
throw new Error("NPM_TOKEN_ORG is not set")
}
if (!process.env.GITHUB_TOKEN) {
throw new Error("GITHUB_TOKEN is not set")
if (!process.env.RELEASE_TOKEN) {
throw new Error("RELEASE_TOKEN is not set")
}
}