Skip to content

Commit

Permalink
fix: update Middleware (#4757)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
balazsorban44 committed Jun 24, 2022
1 parent d8d9ab9 commit 99f5b96
Show file tree
Hide file tree
Showing 17 changed files with 5,536 additions and 6,927 deletions.
@@ -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 @@ -115,13 +115,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

0 comments on commit 99f5b96

Please sign in to comment.