Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: update Middleware #4757

Merged
merged 10 commits into from Jun 24, 2022
Merged
@@ -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"
Expand Down
1 change: 1 addition & 0 deletions apps/dev/next-env.d.ts
@@ -1,4 +1,5 @@
/// <reference types="next" />
/// <reference types="next/dist/styled-jsx-types/global" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
Expand Down
28 changes: 14 additions & 14 deletions apps/dev/package.json
Expand Up @@ -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.45",
"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"
}
}
6 changes: 3 additions & 3 deletions apps/example-gatsby/package.json
Expand Up @@ -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"
Expand Down
12 changes: 12 additions & 0 deletions apps/example-nextjs/middleware.ts
@@ -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"] }
14 changes: 7 additions & 7 deletions apps/example-nextjs/package.json
Expand Up @@ -23,16 +23,16 @@
],
"license": "ISC",
"dependencies": {
"next": "^12.0.11-canary.4",
"next": "12.1.7-canary.45",
"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
Expand Down
@@ -1,4 +1,4 @@
import Layout from "../../components/layout"
import Layout from "../components/layout"

export default function Page() {
return (
Expand Down
8 changes: 0 additions & 8 deletions apps/example-nextjs/pages/admin/_middleware.ts

This file was deleted.

@@ -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()
Expand Down
2 changes: 0 additions & 2 deletions apps/example-nextjs/pages/me/_middleware.ts

This file was deleted.

2 changes: 1 addition & 1 deletion apps/playground-sveltekit/package.json
Expand Up @@ -30,7 +30,7 @@
"type": "module",
"dependencies": {
"cookie": "0.4.1",
"next-auth": "^4.5.0"
"next-auth": "workspace:*"
},
"prettier": {
"semi": false,
Expand Down
120 changes: 58 additions & 62 deletions docs/docs/configuration/nextjs.md
Expand Up @@ -86,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 not 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`

Expand Down Expand Up @@ -172,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)
},
{
Expand All @@ -220,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)
Expand Down
8 changes: 4 additions & 4 deletions packages/next-auth/package.json
Expand Up @@ -114,13 +114,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.45",
"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": {
Expand Down
2 changes: 1 addition & 1 deletion packages/next-auth/src/core/index.ts
Expand Up @@ -14,7 +14,7 @@ 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>
Expand Down
18 changes: 10 additions & 8 deletions packages/next-auth/src/core/lib/cookie.ts
Expand Up @@ -120,22 +120,24 @@ export class SessionStore {
constructor(
option: CookieOption,
req: {
cookies?: Record<string, string> | { get: (key: string) => string }
cookies?: Partial<Record<string, string> | Map<string, string>>
headers?: Headers | IncomingHttpHeaders | Record<string, string>
},
logger: LoggerInstance | Console
) {
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] =
typeof req.cookies.get === "function"
? req.cookies.get(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]
}
}
}
Expand Down
7 changes: 5 additions & 2 deletions packages/next-auth/src/next/middleware.ts
Expand Up @@ -66,14 +66,17 @@ export interface NextAuthMiddlewareOptions {
* @example
*
* ```js
* // `pages/admin/_middleware.js`
* // `middleware.js`
* import { withAuth } from "next-auth/middleware"
*
* export default withAuth({
* callbacks: {
* authorized: ({ token }) => token?.user.isAdmin
* }
* })
*
* export const config = { matcher: ["/admin"] }
*
* ```
*
* ---
Expand Down Expand Up @@ -149,7 +152,7 @@ export type WithAuthArgs =
* @example
*
* ```js
* // `pages/_middleware.js`
* // `middleware.js`
* export { default } from "next-auth/middleware"
* ```
*
Expand Down