Skip to content

Commit f3be5e8

Browse files
feat(middleware): introduce withAuth Next.js method (#3657)
* feat(middleware): introduce Middleware API to Next.js * chore(app): upgrade Next.js in dev app * chore(dev): add Middleware protected page to dev app * chore(middleware): add `next/middleware` to `exports` * fix(middleware): bail out redirect on custom pages * fix(middleware): allow one-line export * chore(middleware): simplify code * fix(middleware): redirect back to page after succesful login * feat(middleware): re-export `withAuth` as `default` * chore: export middleware from `next-auth/middleware` * chore: add `middleware` files to npm * feat(middleware): handle chaining, fix some bugs * chore(dev): showcase different middlewares * chore(middleware): remove `@ts-expect-error` comments * chore: update build clean script * fix: bail out when NextAuth.js paths * refactor: be more explicit about `initConfig` result * refactor: simplify * refactor: use `callbacks` similarily to `NextAuthOptions` * refactor: use `nextauth` namespace when setting `token` on `req` * refactor: don't allow passing `secret` * addressing review
1 parent 844c9b1 commit f3be5e8

File tree

10 files changed

+211
-4
lines changed

10 files changed

+211
-4
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ node_modules
3636
/index.d.ts
3737
/index.js
3838
/next
39+
/middleware.d.ts
40+
/middleware.js
3941

4042
# Development app
4143
app/src/css

app/components/header.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ export default function Header() {
103103
<a>Email</a>
104104
</Link>
105105
</li>
106+
<li className={styles.navItem}>
107+
<Link href="/middleware-protected">
108+
<a>Middleware protected</a>
109+
</Link>
110+
</li>
106111
</ul>
107112
</nav>
108113
</header>

app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"@prisma/client": "^3.7.0",
2323
"fake-smtp-server": "^0.8.0",
2424
"faunadb": "^4.4.1",
25-
"next": "^12.0.7",
25+
"next": "^12.0.8",
2626
"nodemailer": "^6.7.2",
2727
"react": "^17.0.2",
2828
"react-dom": "^17.0.2"
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
export { default } from "next-auth/middleware"
2+
3+
// Other ways to use this middleware
4+
5+
// import withAuth from "next-auth/middleware"
6+
// import { withAuth } from "next-auth/middleware"
7+
8+
// export function middleware(req, ev) {
9+
// return withAuth(req)
10+
// }
11+
12+
// export function middleware(req, ev) {
13+
// return withAuth(req, ev)
14+
// }
15+
16+
// export function middleware(req, ev) {
17+
// return withAuth(req, {
18+
// callbacks: {
19+
// authorized: ({ token }) => !!token,
20+
// },
21+
// })
22+
// }
23+
24+
// export default withAuth(function middleware(req, ev) {
25+
// console.log(req.nextauth.token)
26+
// })
27+
28+
// export default withAuth(
29+
// function middleware(req, ev) {
30+
// console.log(req, ev)
31+
// return undefined // NOTE: `NextMiddleware` should allow returning `void`
32+
// },
33+
// {
34+
// callbacks: {
35+
// authorized: ({ token }) => token.name === "Balázs Orbán",
36+
// }
37+
// }
38+
// )
39+
40+
// export default withAuth({
41+
// callbacks: {
42+
// authorized: ({ token }) => !!token,
43+
// },
44+
// })
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Layout from "components/layout"
2+
3+
export default function Page() {
4+
return (
5+
<Layout>
6+
<h1>Page protected by Middleware</h1>
7+
</Layout>
8+
)
9+
}

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,13 @@
3131
"./react": "./react/index.js",
3232
"./core": "./core/index.js",
3333
"./next": "./next/index.js",
34+
"./middleware": "./middleware.js",
3435
"./client/_utils": "./client/_utils.js",
3536
"./providers/*": "./providers/*.js"
3637
},
3738
"scripts": {
3839
"build": "npm run build:js && npm run build:css",
39-
"clean": "rm -rf client css lib providers core jwt react next index.d.ts index.js adapters.d.ts",
40+
"clean": "rm -rf client css lib providers core jwt react next index.d.ts index.js adapters.d.ts middleware.d.ts middleware.js",
4041
"build:js": "npm run clean && npm run generate-providers && tsc && babel --config-file ./config/babel.config.js src --out-dir . --extensions \".tsx,.ts,.js,.jsx\"",
4142
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir . && node config/wrap-css.js",
4243
"dev:setup": "npm i && npm run generate-providers && npm run build:css && cd app && npm i",
@@ -61,7 +62,9 @@
6162
"core",
6263
"index.d.ts",
6364
"index.js",
64-
"adapters.d.ts"
65+
"adapters.d.ts",
66+
"middleware.d.ts",
67+
"middleware.js"
6568
],
6669
"license": "ISC",
6770
"dependencies": {

src/jwt/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export async function decode(params: JWTDecodeParams): Promise<JWT | null> {
3737

3838
export interface GetTokenParams<R extends boolean = false> {
3939
/** The request containing the JWT either in the cookies or in the `Authorization` header. */
40-
req: NextApiRequest
40+
req: NextApiRequest | Pick<NextApiRequest, "cookies" | "headers">
4141
/**
4242
* Use secure prefix for cookie name, unless URL in `NEXTAUTH_URL` is http://
4343
* or not set (e.g. development or test instance) case use unprefixed name

src/lib/parse-url.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface InternalUrl {
1111
toString: () => string
1212
}
1313

14+
/** Returns an `URL` like object to make requests/redirects from server-side */
1415
export default function parseUrl(url?: string): InternalUrl {
1516
const defaultUrl = new URL("http://localhost:3000/api/auth")
1617

src/middleware.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from "./next/middleware"
2+
export * from "./next/middleware"

src/next/middleware.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import type { NextMiddleware, NextFetchEvent } from "next/server"
2+
import type { Awaitable, NextAuthOptions } from ".."
3+
import type { JWT } from "../jwt"
4+
5+
import { NextResponse, NextRequest } from "next/server"
6+
7+
import { getToken } from "../jwt"
8+
import parseUrl from "../lib/parse-url"
9+
10+
type AuthorizedCallback = (params: {
11+
token: JWT | null
12+
req: NextRequest
13+
}) => Awaitable<boolean>
14+
15+
export interface NextAuthMiddlewareOptions {
16+
/**
17+
* Where to redirect the user in case of an error if they weren't logged in.
18+
* Similar to `pages` in `NextAuth`.
19+
*
20+
* ---
21+
* [Documentation](https://next-auth.js.org/configuration/pages)
22+
*/
23+
pages?: NextAuthOptions["pages"]
24+
callbacks?: {
25+
/**
26+
* Callback that receives the user's JWT payload
27+
* and returns `true` to allow the user to continue.
28+
*
29+
* This is similar to the `signIn` callback in `NextAuthOptions`.
30+
*
31+
* If it returns `false`, the user is redirected to the sign-in page instead
32+
*
33+
* The default is to let the user continue if they have a valid JWT (basic authentication).
34+
*
35+
* How to restrict a page and all of it's subpages for admins-only:
36+
* @example
37+
*
38+
* ```js
39+
* // `pages/admin/_middleware.js`
40+
* import { withAuth } from "next-auth/middleware"
41+
*
42+
* export default withAuth({
43+
* callbacks: {
44+
* authorized: ({ token }) => token?.user.isAdmin
45+
* }
46+
* })
47+
* ```
48+
*
49+
* ---
50+
* [Documentation](https://next-auth.js.org/getting-started/nextjs/middleware#api) | [`signIn` callback](configuration/callbacks#sign-in-callback)
51+
*/
52+
authorized?: AuthorizedCallback
53+
}
54+
}
55+
56+
async function handleMiddleware(
57+
req: NextRequest,
58+
options: NextAuthMiddlewareOptions | undefined,
59+
onSuccess?: (token: JWT | null) => Promise<any>
60+
) {
61+
const signInPage = options?.pages?.signIn ?? "/api/auth/signin"
62+
const errorPage = options?.pages?.error ?? "/api/auth/error"
63+
const basePath = parseUrl(process.env.NEXTAUTH_URL).path
64+
// Avoid infinite redirect loop
65+
if (
66+
req.nextUrl.pathname.startsWith(basePath) ||
67+
[signInPage, errorPage].includes(req.nextUrl.pathname)
68+
) {
69+
return
70+
}
71+
72+
if (!process.env.NEXTAUTH_SECRET) {
73+
console.error(
74+
`[next-auth][error][NO_SECRET]`,
75+
`\nhttps://next-auth.js.org/errors#no_secret`
76+
)
77+
78+
return {
79+
redirect: NextResponse.redirect(`${errorPage}?error=Configuration`),
80+
}
81+
}
82+
83+
const token = await getToken({ req: req as any })
84+
85+
const isAuthorized =
86+
(await options?.callbacks?.authorized?.({ req, token })) ?? !!token
87+
88+
// the user is authorized, let the middleware handle the rest
89+
if (isAuthorized) return await onSuccess?.(token)
90+
91+
// the user is not logged in, re-direct to the sign-in page
92+
return NextResponse.redirect(
93+
`${signInPage}?${new URLSearchParams({ callbackUrl: req.url })}`
94+
)
95+
}
96+
97+
export type WithAuthArgs =
98+
| [NextRequest]
99+
| [NextRequest, NextFetchEvent]
100+
| [NextRequest, NextAuthMiddlewareOptions]
101+
| [NextMiddleware]
102+
| [NextMiddleware, NextAuthMiddlewareOptions]
103+
| [NextAuthMiddlewareOptions]
104+
105+
/**
106+
* Middleware that checks if the user is authenticated/authorized.
107+
* If if they aren't, they will be redirected to the login page.
108+
* Otherwise, continue.
109+
*
110+
* @example
111+
*
112+
* ```js
113+
* // `pages/_middleware.js`
114+
* export { default } from "next-auth/middleware"
115+
* ```
116+
*
117+
* ---
118+
* [Documentation](https://next-auth.js.org/getting-started/middleware)
119+
*/
120+
export function withAuth(...args: WithAuthArgs) {
121+
if (args[0] instanceof NextRequest) {
122+
// @ts-expect-error
123+
return handleMiddleware(...args)
124+
}
125+
126+
if (typeof args[0] === "function") {
127+
const middleware = args[0]
128+
const options = args[1] as NextAuthMiddlewareOptions | undefined
129+
return async (...args: Parameters<NextMiddleware>) =>
130+
await handleMiddleware(args[0], options, async (token) => {
131+
;(args[0] as any).nextauth = { token }
132+
return await middleware(...args)
133+
})
134+
}
135+
136+
const options = args[0]
137+
return async (...args: Parameters<NextMiddleware>) =>
138+
await handleMiddleware(args[0], options)
139+
}
140+
141+
export default withAuth

0 commit comments

Comments
 (0)