From b49686600d6126490c663a0e3e37ce0162b431ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Tue, 18 Jan 2022 01:31:43 +0100 Subject: [PATCH 01/22] feat(middleware): introduce Middleware API to Next.js --- src/jwt/index.ts | 2 +- src/next/middleware.ts | 97 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/next/middleware.ts diff --git a/src/jwt/index.ts b/src/jwt/index.ts index f479517936..1584c9b984 100644 --- a/src/jwt/index.ts +++ b/src/jwt/index.ts @@ -42,7 +42,7 @@ export async function decode({ export interface GetTokenParams { /** The request containing the JWT either in the cookies or in the `Authorization` header. */ - req: NextApiRequest + req: NextApiRequest | Pick /** * Use secure prefix for cookie name, unless URL in `NEXTAUTH_URL` is http:// * or not set (e.g. development or test instance) case use unprefixed name diff --git a/src/next/middleware.ts b/src/next/middleware.ts new file mode 100644 index 0000000000..ab6eeb826f --- /dev/null +++ b/src/next/middleware.ts @@ -0,0 +1,97 @@ +import type { NextRequest } from "next/server" +import type { Awaitable, NextAuthOptions } from ".." +import type { JWT } from "../jwt" + +import { NextResponse } from "next/server" +import { getToken } from "../jwt" + +export interface NextAuthMiddlewareOptions { + /** + * The secret used to create the session. + * @note Must match as `secret` in `NextAuth`. + * @default process.env.NEXTAUTH_SECRET + * + * --- + * [Documentation](https://next-auth.js.org/configuration/options#secret) + */ + secret?: string + /** + * Where to redirect the user in case of an error if they weren't logged in. + * Similar to `pages` in `NextAuth`. + * + * --- + * [Documentation](https://next-auth.js.org/configuration/pages) + */ + pages?: NextAuthOptions["pages"] + /** + * Callback that receives the user's JWT payload + * and returns `true` to allow the user to continue. + * + * If it returns `false`, the user is redirected to the sign-in page instead + * + * The default is to let the user continue if they have a valid JWT (basic authentication). + * + * How to restrict a page and all of it's subpages for admins-only: + * @example + * + * ```js + * // `pages/admin/_middleware.js` + * import { withAuth } from "next-auth/next/middleware" + * + * export default withAuth({ + * authorized: ({ token }) => token?.user.isAdmin + * }) + * ``` + */ + authorized: (options: { + token: JWT | null + req: NextRequest + }) => Awaitable +} + +/** + * Middleware that checks if the user is authenticated/authorized. + * If if they aren't, they will be redirected to the login page. + * Otherwise, continue. + * + * @example + * + * ```js + * // `pages/_middleware.js` + * export { withAuth as default } from "next-auth/next/middleware" + * ``` + * + * --- + * [Documentation](https://next-auth.js.org/getting-started/middleware) + */ +export function withAuth(options?: NextAuthMiddlewareOptions) { + const secret = options?.secret ?? process.env.NEXTAUTH_SECRET + if (!secret) { + const code = "NO_SECRET" + console.error( + `[next-auth][error][${code}]`, + `\nhttps://next-auth.js.org/errors#${code.toLowerCase()}` + ) + return NextResponse.redirect( + options?.pages?.error ?? "/api/auth/error?error=Configuration" + ) + } + + return async function middleware(req: NextRequest) { + const token = await getToken({ + req: { + headers: req.headers as any, + cookies: req.cookies, + }, + secret, + }) + + const isAuthorized = options?.authorized + ? options?.authorized?.({ token, req }) + : !!token + + if (isAuthorized) return + + return NextResponse.redirect(options?.pages?.signIn ?? "/api/auth/signin") + } +} From 13ce4deee809b1aa3f77a7c930265ce1bc0a9579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Tue, 18 Jan 2022 01:32:07 +0100 Subject: [PATCH 02/22] chore(app): upgrade Next.js in dev app --- app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/package.json b/app/package.json index efc7f11fc4..14b83c06c8 100644 --- a/app/package.json +++ b/app/package.json @@ -22,7 +22,7 @@ "@prisma/client": "^3.7.0", "fake-smtp-server": "^0.8.0", "faunadb": "^4.4.1", - "next": "^12.0.7", + "next": "^12.0.8", "nodemailer": "^6.7.2", "react": "^17.0.2", "react-dom": "^17.0.2" From d1337a57bf90f1a3d4ca916d62fc4f2f16adccd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Tue, 18 Jan 2022 01:32:21 +0100 Subject: [PATCH 03/22] chore(dev): add Middleware protected page to dev app --- app/components/header.js | 5 +++++ app/pages/middleware-protected/_middleware.js | 1 + app/pages/middleware-protected/index.js | 9 +++++++++ 3 files changed, 15 insertions(+) create mode 100644 app/pages/middleware-protected/_middleware.js create mode 100644 app/pages/middleware-protected/index.js diff --git a/app/components/header.js b/app/components/header.js index b3b0e2a4a2..796bb0b158 100644 --- a/app/components/header.js +++ b/app/components/header.js @@ -103,6 +103,11 @@ export default function Header() { Email +
  • + + Middleware protected + +
  • diff --git a/app/pages/middleware-protected/_middleware.js b/app/pages/middleware-protected/_middleware.js new file mode 100644 index 0000000000..3bbd835687 --- /dev/null +++ b/app/pages/middleware-protected/_middleware.js @@ -0,0 +1 @@ +export { withAuth as default } from "next-auth/next/middleware" diff --git a/app/pages/middleware-protected/index.js b/app/pages/middleware-protected/index.js new file mode 100644 index 0000000000..525019d56d --- /dev/null +++ b/app/pages/middleware-protected/index.js @@ -0,0 +1,9 @@ +import Layout from "components/layout" + +export default function Page() { + return ( + +

    Page protected by Middleware

    +
    + ) +} From f9be36b821c4a23b20c170f800962b8d11f15482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Tue, 18 Jan 2022 01:48:17 +0100 Subject: [PATCH 04/22] chore(middleware): add `next/middleware` to `exports` --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 05462e0ce6..da66f0fd1f 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "./react": "./react/index.js", "./core": "./core/index.js", "./next": "./next/index.js", + "./next/middleware": "./next/middleware.js", "./client/_utils": "./client/_utils.js", "./providers/*": "./providers/*.js" }, From 061e7ba0b34ba41d92ac7b7ccd25ab13348c6714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Tue, 18 Jan 2022 01:56:24 +0100 Subject: [PATCH 05/22] fix(middleware): bail out redirect on custom pages --- src/next/middleware.ts | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/next/middleware.ts b/src/next/middleware.ts index ab6eeb826f..0077e17257 100644 --- a/src/next/middleware.ts +++ b/src/next/middleware.ts @@ -66,18 +66,26 @@ export interface NextAuthMiddlewareOptions { */ export function withAuth(options?: NextAuthMiddlewareOptions) { const secret = options?.secret ?? process.env.NEXTAUTH_SECRET - if (!secret) { - const code = "NO_SECRET" - console.error( - `[next-auth][error][${code}]`, - `\nhttps://next-auth.js.org/errors#${code.toLowerCase()}` - ) - return NextResponse.redirect( - options?.pages?.error ?? "/api/auth/error?error=Configuration" - ) - } - return async function middleware(req: NextRequest) { + const pages = options?.pages ?? {} + + // Don't trigger infinite redirect on custom pages. + const { pathname } = req.nextUrl + if (pathname === pages.signIn || pathname === pages.error) { + return + } + + if (!secret) { + const code = "NO_SECRET" + console.error( + `[next-auth][error][${code}]`, + `\nhttps://next-auth.js.org/errors#${code.toLowerCase()}` + ) + return NextResponse.redirect( + options?.pages?.error ?? "/api/auth/error?error=Configuration" + ) + } + const token = await getToken({ req: { headers: req.headers as any, @@ -92,6 +100,6 @@ export function withAuth(options?: NextAuthMiddlewareOptions) { if (isAuthorized) return - return NextResponse.redirect(options?.pages?.signIn ?? "/api/auth/signin") + return NextResponse.redirect(pages.signIn ?? "/api/auth/signin") } } From f4b773345dea38b52f1ab53aa50512ef4efc587f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Tue, 18 Jan 2022 02:44:39 +0100 Subject: [PATCH 06/22] fix(middleware): allow one-line export --- src/next/middleware.ts | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/next/middleware.ts b/src/next/middleware.ts index 0077e17257..6f1591b123 100644 --- a/src/next/middleware.ts +++ b/src/next/middleware.ts @@ -1,4 +1,4 @@ -import type { NextRequest } from "next/server" +import type { NextFetchEvent, NextRequest } from "next/server" import type { Awaitable, NextAuthOptions } from ".." import type { JWT } from "../jwt" @@ -64,7 +64,35 @@ export interface NextAuthMiddlewareOptions { * --- * [Documentation](https://next-auth.js.org/getting-started/middleware) */ -export function withAuth(options?: NextAuthMiddlewareOptions) { +export async function withAuth( + ...args: [NextAuthMiddlewareOptions] | [NextRequest, NextFetchEvent] +) { + if (args.length === 2) { + const secret = process.env.NEXTAUTH_SECRET + + if (!secret) { + const code = "NO_SECRET" + console.error( + `[next-auth][error][${code}]`, + `\nhttps://next-auth.js.org/errors#${code.toLowerCase()}` + ) + return NextResponse.redirect("/api/auth/error?error=Configuration") + } + + const req = args[0] + const token = await getToken({ + req: { + headers: req.headers as any, + cookies: req.cookies, + }, + secret, + }) + if (token) return + + return NextResponse.redirect("/api/auth/signin") + } + + const options = args[0] const secret = options?.secret ?? process.env.NEXTAUTH_SECRET return async function middleware(req: NextRequest) { const pages = options?.pages ?? {} From ea16ed8e4792eaab8be3bd4d79266345ecd1aa18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Tue, 18 Jan 2022 02:48:58 +0100 Subject: [PATCH 07/22] chore(middleware): simplify code --- src/next/middleware.ts | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/next/middleware.ts b/src/next/middleware.ts index 6f1591b123..0b266d8b04 100644 --- a/src/next/middleware.ts +++ b/src/next/middleware.ts @@ -79,15 +79,7 @@ export async function withAuth( return NextResponse.redirect("/api/auth/error?error=Configuration") } - const req = args[0] - const token = await getToken({ - req: { - headers: req.headers as any, - cookies: req.cookies, - }, - secret, - }) - if (token) return + if (await getToken({ req: args[0] as any, secret })) return return NextResponse.redirect("/api/auth/signin") } @@ -114,13 +106,7 @@ export async function withAuth( ) } - const token = await getToken({ - req: { - headers: req.headers as any, - cookies: req.cookies, - }, - secret, - }) + const token = await getToken({ req: req as any, secret }) const isAuthorized = options?.authorized ? options?.authorized?.({ token, req }) From 96d07ab7c73fcf801d253ad1c49177b0b603cdc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Tue, 18 Jan 2022 02:53:09 +0100 Subject: [PATCH 08/22] fix(middleware): redirect back to page after succesful login --- src/next/middleware.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/next/middleware.ts b/src/next/middleware.ts index 0b266d8b04..bdf9c84729 100644 --- a/src/next/middleware.ts +++ b/src/next/middleware.ts @@ -78,10 +78,12 @@ export async function withAuth( ) return NextResponse.redirect("/api/auth/error?error=Configuration") } + const req = args[0] + if (await getToken({ req: req as any, secret })) return - if (await getToken({ req: args[0] as any, secret })) return - - return NextResponse.redirect("/api/auth/signin") + return NextResponse.redirect( + `/api/auth/signin?${new URLSearchParams({ callbackUrl: req.url })}` + ) } const options = args[0] @@ -114,6 +116,9 @@ export async function withAuth( if (isAuthorized) return - return NextResponse.redirect(pages.signIn ?? "/api/auth/signin") + const redirectUrl = pages.signIn ?? "/api/auth/signin" + return NextResponse.redirect( + `${redirectUrl}?${new URLSearchParams({ callbackUrl: req.url })}` + ) } } From 6c27a82b1eaab6811402f9bb0ccbcb73185c66d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 19 Jan 2022 00:55:30 +0100 Subject: [PATCH 09/22] feat(middleware): re-export `withAuth` as `default` --- app/pages/middleware-protected/_middleware.js | 2 +- src/next/middleware.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/pages/middleware-protected/_middleware.js b/app/pages/middleware-protected/_middleware.js index 3bbd835687..bc63280694 100644 --- a/app/pages/middleware-protected/_middleware.js +++ b/app/pages/middleware-protected/_middleware.js @@ -1 +1 @@ -export { withAuth as default } from "next-auth/next/middleware" +export { default } from "next-auth/next/middleware" diff --git a/src/next/middleware.ts b/src/next/middleware.ts index bdf9c84729..596879b268 100644 --- a/src/next/middleware.ts +++ b/src/next/middleware.ts @@ -122,3 +122,5 @@ export async function withAuth( ) } } + +export default withAuth From a7ca9e3c9d004c9e6df573c7fdbd8894cb824679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 19 Jan 2022 01:02:14 +0100 Subject: [PATCH 10/22] chore: export middleware from `next-auth/middleware` --- .gitignore | 2 ++ package.json | 4 ++-- src/middleware.ts | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 src/middleware.ts diff --git a/.gitignore b/.gitignore index 669d8d4f99..9c2bcf5992 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,8 @@ node_modules /index.d.ts /index.js /next +/middleware.d.ts +/middleware.js # Development app app/src/css diff --git a/package.json b/package.json index da66f0fd1f..d8bb6f3291 100644 --- a/package.json +++ b/package.json @@ -31,13 +31,13 @@ "./react": "./react/index.js", "./core": "./core/index.js", "./next": "./next/index.js", - "./next/middleware": "./next/middleware.js", + "./middleware": "./middleware.js", "./client/_utils": "./client/_utils.js", "./providers/*": "./providers/*.js" }, "scripts": { "build": "npm run build:js && npm run build:css", - "clean": "rm -rf client css lib providers core jwt react next index.d.ts index.js adapters.d.ts", + "clean": "rm -rf client css lib providers core jwt react next index.d.ts index.js adapters.d.ts middleware.d.ts", "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\"", "build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir . && node config/wrap-css.js", "dev:setup": "npm i && npm run generate-providers && npm run build:css && cd app && npm i", diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000000..2bb2f9965a --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,2 @@ +export { default } from "./next/middleware" +export * from "./next/middleware" From 9c44d13484f0b2c0f0a710e48e1d49a6d9c52717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 19 Jan 2022 01:08:21 +0100 Subject: [PATCH 11/22] chore: add `middleware` files to npm --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index d8bb6f3291..b7d2738eb9 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,9 @@ "core", "index.d.ts", "index.js", - "adapters.d.ts" + "adapters.d.ts", + "middleware.d.ts", + "middleware.js" ], "license": "ISC", "dependencies": { From e32e78289e24d8690a6b56b3cb242c2db81417e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Fri, 21 Jan 2022 02:58:50 +0100 Subject: [PATCH 12/22] feat(middleware): handle chaining, fix some bugs --- src/next/middleware.ts | 131 ++++++++++++++++++++++++++--------------- 1 file changed, 85 insertions(+), 46 deletions(-) diff --git a/src/next/middleware.ts b/src/next/middleware.ts index 596879b268..541f6ecb20 100644 --- a/src/next/middleware.ts +++ b/src/next/middleware.ts @@ -1,8 +1,9 @@ -import type { NextFetchEvent, NextRequest } from "next/server" +import type { NextMiddleware, NextFetchEvent } from "next/server" import type { Awaitable, NextAuthOptions } from ".." import type { JWT } from "../jwt" -import { NextResponse } from "next/server" +import { NextResponse, NextRequest } from "next/server" + import { getToken } from "../jwt" export interface NextAuthMiddlewareOptions { @@ -43,12 +44,56 @@ export interface NextAuthMiddlewareOptions { * }) * ``` */ - authorized: (options: { + authorized?: (options: { token: JWT | null req: NextRequest }) => Awaitable } +export type WithAuthArgs = + | [NextRequest] + | [NextRequest, NextFetchEvent] + | [NextRequest, NextAuthMiddlewareOptions] + | [NextMiddleware] + | [NextMiddleware, NextAuthMiddlewareOptions] + | [NextAuthMiddlewareOptions] + +/** Check if `secret` has been declared */ +function initConfig( + req: NextRequest, + options?: NextAuthMiddlewareOptions | NextFetchEvent +) { + // @ts-expect-error + const signInPage = options?.pages?.signIn ?? "/api/auth/signin" + // @ts-expect-error + const errorPage = options?.pages?.error ?? "/api/auth/error" + + // Avoid infinite redirect loop + if ([signInPage, errorPage].includes(req.nextUrl.pathname)) return + + // @ts-expect-error + const secret = options?.secret ?? process.env.NEXTAUTH_SECRET + // Continue only if the secret is specified + if (secret) + return { + req, + options, + secret, + signInPage, + // @ts-expect-error + authorized: options?.authorized || (({ token }) => token), + } + + console.error( + `[next-auth][error][NO_SECRET]`, + `\nhttps://next-auth.js.org/errors#no_secret` + ) + + return { + redirect: NextResponse.redirect(`${errorPage}?error=Configuration`), + } +} + /** * Middleware that checks if the user is authenticated/authorized. * If if they aren't, they will be redirected to the login page. @@ -58,67 +103,61 @@ export interface NextAuthMiddlewareOptions { * * ```js * // `pages/_middleware.js` - * export { withAuth as default } from "next-auth/next/middleware" + * export { default } from "next-auth/middleware" * ``` * * --- * [Documentation](https://next-auth.js.org/getting-started/middleware) */ -export async function withAuth( - ...args: [NextAuthMiddlewareOptions] | [NextRequest, NextFetchEvent] -) { - if (args.length === 2) { - const secret = process.env.NEXTAUTH_SECRET - - if (!secret) { - const code = "NO_SECRET" - console.error( - `[next-auth][error][${code}]`, - `\nhttps://next-auth.js.org/errors#${code.toLowerCase()}` - ) - return NextResponse.redirect("/api/auth/error?error=Configuration") - } - const req = args[0] - if (await getToken({ req: req as any, secret })) return +export function withAuth(...args: WithAuthArgs) { + if (args[0] instanceof NextRequest) { + const config = initConfig(args[0], args[1]) + if (!config || config.redirect) return config?.redirect + const { req, secret, signInPage, authorized } = config - return NextResponse.redirect( - `/api/auth/signin?${new URLSearchParams({ callbackUrl: req.url })}` - ) + return getToken({ req: req as any, secret }).then(async (token) => { + if (await authorized({ req, token })) return + + return NextResponse.redirect( + `${signInPage}?${new URLSearchParams({ callbackUrl: req.url })}` + ) + }) } - const options = args[0] - const secret = options?.secret ?? process.env.NEXTAUTH_SECRET - return async function middleware(req: NextRequest) { - const pages = options?.pages ?? {} + if (typeof args[0] === "function") { + const middleware = args[0] + const options = args[1] as NextAuthMiddlewareOptions | undefined + return async (...args: Parameters) => { + const config = initConfig(args[0], options) + if (!config || config.redirect) return config?.redirect + const { req, secret, signInPage, authorized } = config - // Don't trigger infinite redirect on custom pages. - const { pathname } = req.nextUrl - if (pathname === pages.signIn || pathname === pages.error) { - return - } + const token = await getToken({ req: req as any, secret }) + + if (await authorized({ req, token })) { + ;(args[0] as any).token = token + return await middleware(...args) + } - if (!secret) { - const code = "NO_SECRET" - console.error( - `[next-auth][error][${code}]`, - `\nhttps://next-auth.js.org/errors#${code.toLowerCase()}` - ) return NextResponse.redirect( - options?.pages?.error ?? "/api/auth/error?error=Configuration" + `${signInPage}?${new URLSearchParams({ callbackUrl: req.url })}` ) } + } - const token = await getToken({ req: req as any, secret }) + const options = args[0] + return async (...args: Parameters) => { + const config = initConfig(args[0], options) + if (!config || config.redirect) return config?.redirect + + const { req, secret, signInPage, authorized } = config - const isAuthorized = options?.authorized - ? options?.authorized?.({ token, req }) - : !!token + const token = await getToken({ req: req as any, secret }) - if (isAuthorized) return + if (await authorized({ req, token })) return - const redirectUrl = pages.signIn ?? "/api/auth/signin" return NextResponse.redirect( - `${redirectUrl}?${new URLSearchParams({ callbackUrl: req.url })}` + `${signInPage}?${new URLSearchParams({ callbackUrl: req.url })}` ) } } From e767e0ee09aa8a8d21d726ec39fc8fafbfc8e5a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Fri, 21 Jan 2022 02:59:03 +0100 Subject: [PATCH 13/22] chore(dev): showcase different middlewares --- app/pages/middleware-protected/_middleware.js | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/app/pages/middleware-protected/_middleware.js b/app/pages/middleware-protected/_middleware.js index bc63280694..4f39389dd0 100644 --- a/app/pages/middleware-protected/_middleware.js +++ b/app/pages/middleware-protected/_middleware.js @@ -1 +1,41 @@ -export { default } from "next-auth/next/middleware" +export { default } from "next-auth/middleware" + +// Other ways to use this middleware + +// import withAuth from "next-auth/middleware" +// import { withAuth } from "next-auth/middleware" + +// export function middleware(req, ev) { +// return withAuth(req) +// } + +// export function middleware(req, ev) { +// return withAuth(req, ev) +// } + +// export function middleware(req, ev) { +// return withAuth(req, { +// secret: process.env.NEXTAUTH_SECRET, +// authorized: ({ token }) => !!token, +// }) +// } + +// export default withAuth(function middleware(req, ev) { +// console.log(req.token) +// }) + +// export default withAuth( +// function middleware(req, ev) { +// console.log(req, ev) +// return undefined // NOTE: `NextMiddleware` should allow returning `void` +// }, +// { +// secret: process.env.NEXTAUTH_SECRET, +// authorized: ({ token }) => token.name === "Balázs Orbán", +// } +// ) + +// export default withAuth({ +// secret: process.env.NEXTAUTH_SECRET, +// authorized: ({ token }) => !!token, +// }) From c3d58cb33906ffbf3a51284137ed33a262b6fa1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Fri, 21 Jan 2022 03:01:53 +0100 Subject: [PATCH 14/22] chore(middleware): remove `@ts-expect-error` comments --- src/next/middleware.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/next/middleware.ts b/src/next/middleware.ts index 541f6ecb20..5a7631d008 100644 --- a/src/next/middleware.ts +++ b/src/next/middleware.ts @@ -61,17 +61,15 @@ export type WithAuthArgs = /** Check if `secret` has been declared */ function initConfig( req: NextRequest, - options?: NextAuthMiddlewareOptions | NextFetchEvent + optionsOrEv?: NextAuthMiddlewareOptions | NextFetchEvent ) { - // @ts-expect-error + const options = optionsOrEv as NextAuthMiddlewareOptions // This is relatively safe as we don't share properties with NextFetchEvent const signInPage = options?.pages?.signIn ?? "/api/auth/signin" - // @ts-expect-error const errorPage = options?.pages?.error ?? "/api/auth/error" // Avoid infinite redirect loop if ([signInPage, errorPage].includes(req.nextUrl.pathname)) return - // @ts-expect-error const secret = options?.secret ?? process.env.NEXTAUTH_SECRET // Continue only if the secret is specified if (secret) @@ -80,8 +78,7 @@ function initConfig( options, secret, signInPage, - // @ts-expect-error - authorized: options?.authorized || (({ token }) => token), + authorized: options?.authorized ?? (({ token }) => token), } console.error( From 2976116e6b8d94cd1b3d336355548c9c11ed8115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Fri, 21 Jan 2022 03:03:21 +0100 Subject: [PATCH 15/22] chore: update build clean script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b7d2738eb9..c2c7eb7a5c 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ }, "scripts": { "build": "npm run build:js && npm run build:css", - "clean": "rm -rf client css lib providers core jwt react next index.d.ts index.js adapters.d.ts middleware.d.ts", + "clean": "rm -rf client css lib providers core jwt react next index.d.ts index.js adapters.d.ts middleware.d.ts middleware.js", "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\"", "build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir . && node config/wrap-css.js", "dev:setup": "npm i && npm run generate-providers && npm run build:css && cd app && npm i", From eb36ed51e3ec309ddafa6996618463131b07d985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 2 Feb 2022 12:52:30 +0100 Subject: [PATCH 16/22] fix: bail out when NextAuth.js paths --- src/lib/parse-url.ts | 1 + src/next/middleware.ts | 37 ++++++++++++++++++++++--------------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/lib/parse-url.ts b/src/lib/parse-url.ts index fbda0b9d9d..6494c097d6 100644 --- a/src/lib/parse-url.ts +++ b/src/lib/parse-url.ts @@ -11,6 +11,7 @@ export interface InternalUrl { toString: () => string } +/** Returns an `URL` like object to make requests/redirects from server-side */ export default function parseUrl(url?: string): InternalUrl { const defaultUrl = new URL("http://localhost:3000/api/auth") diff --git a/src/next/middleware.ts b/src/next/middleware.ts index 5a7631d008..e37db7b51d 100644 --- a/src/next/middleware.ts +++ b/src/next/middleware.ts @@ -5,6 +5,7 @@ import type { JWT } from "../jwt" import { NextResponse, NextRequest } from "next/server" import { getToken } from "../jwt" +import parseUrl from "src/lib/parse-url" export interface NextAuthMiddlewareOptions { /** @@ -66,28 +67,34 @@ function initConfig( const options = optionsOrEv as NextAuthMiddlewareOptions // This is relatively safe as we don't share properties with NextFetchEvent const signInPage = options?.pages?.signIn ?? "/api/auth/signin" const errorPage = options?.pages?.error ?? "/api/auth/error" - + const basePath = parseUrl(process.env.NEXTAUTH_URL).path // Avoid infinite redirect loop - if ([signInPage, errorPage].includes(req.nextUrl.pathname)) return + if ( + req.nextUrl.pathname.startsWith(basePath) || + [signInPage, errorPage].includes(req.nextUrl.pathname) + ) { + return + } const secret = options?.secret ?? process.env.NEXTAUTH_SECRET - // Continue only if the secret is specified - if (secret) + if (!secret) { + console.error( + `[next-auth][error][NO_SECRET]`, + `\nhttps://next-auth.js.org/errors#no_secret` + ) + return { - req, - options, - secret, - signInPage, - authorized: options?.authorized ?? (({ token }) => token), + redirect: NextResponse.redirect(`${errorPage}?error=Configuration`), } + } - console.error( - `[next-auth][error][NO_SECRET]`, - `\nhttps://next-auth.js.org/errors#no_secret` - ) - + // Continue only if the secret is specified return { - redirect: NextResponse.redirect(`${errorPage}?error=Configuration`), + req, + options, + secret, + signInPage, + authorized: options?.authorized ?? (({ token }) => token), } } From cb0ba9edd0e73d150958002203999efe979f24a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 2 Feb 2022 13:00:41 +0100 Subject: [PATCH 17/22] refactor: be more explicit about `initConfig` result --- src/next/middleware.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/next/middleware.ts b/src/next/middleware.ts index e37db7b51d..c9081e7467 100644 --- a/src/next/middleware.ts +++ b/src/next/middleware.ts @@ -116,7 +116,9 @@ function initConfig( export function withAuth(...args: WithAuthArgs) { if (args[0] instanceof NextRequest) { const config = initConfig(args[0], args[1]) - if (!config || config.redirect) return config?.redirect + if (!config) return + if (config.redirect) return config.redirect + const { req, secret, signInPage, authorized } = config return getToken({ req: req as any, secret }).then(async (token) => { @@ -133,7 +135,9 @@ export function withAuth(...args: WithAuthArgs) { const options = args[1] as NextAuthMiddlewareOptions | undefined return async (...args: Parameters) => { const config = initConfig(args[0], options) - if (!config || config.redirect) return config?.redirect + if (!config) return + if (config.redirect) return config.redirect + const { req, secret, signInPage, authorized } = config const token = await getToken({ req: req as any, secret }) @@ -152,7 +156,8 @@ export function withAuth(...args: WithAuthArgs) { const options = args[0] return async (...args: Parameters) => { const config = initConfig(args[0], options) - if (!config || config.redirect) return config?.redirect + if (!config) return + if (config.redirect) return config.redirect const { req, secret, signInPage, authorized } = config From 1a3aac59d73c6f235758dd711f704ec7d2f290c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 2 Feb 2022 13:30:20 +0100 Subject: [PATCH 18/22] refactor: simplify --- src/next/middleware.ts | 95 +++++++++++++----------------------------- 1 file changed, 30 insertions(+), 65 deletions(-) diff --git a/src/next/middleware.ts b/src/next/middleware.ts index c9081e7467..d922c46f49 100644 --- a/src/next/middleware.ts +++ b/src/next/middleware.ts @@ -5,7 +5,7 @@ import type { JWT } from "../jwt" import { NextResponse, NextRequest } from "next/server" import { getToken } from "../jwt" -import parseUrl from "src/lib/parse-url" +import parseUrl from "../lib/parse-url" export interface NextAuthMiddlewareOptions { /** @@ -51,20 +51,11 @@ export interface NextAuthMiddlewareOptions { }) => Awaitable } -export type WithAuthArgs = - | [NextRequest] - | [NextRequest, NextFetchEvent] - | [NextRequest, NextAuthMiddlewareOptions] - | [NextMiddleware] - | [NextMiddleware, NextAuthMiddlewareOptions] - | [NextAuthMiddlewareOptions] - -/** Check if `secret` has been declared */ -function initConfig( +async function handleMiddleware( req: NextRequest, - optionsOrEv?: NextAuthMiddlewareOptions | NextFetchEvent + options: NextAuthMiddlewareOptions | undefined, + onSuccess?: (token: JWT | null) => Promise ) { - const options = optionsOrEv as NextAuthMiddlewareOptions // This is relatively safe as we don't share properties with NextFetchEvent const signInPage = options?.pages?.signIn ?? "/api/auth/signin" const errorPage = options?.pages?.error ?? "/api/auth/error" const basePath = parseUrl(process.env.NEXTAUTH_URL).path @@ -88,16 +79,27 @@ function initConfig( } } - // Continue only if the secret is specified - return { - req, - options, - secret, - signInPage, - authorized: options?.authorized ?? (({ token }) => token), - } + const token = await getToken({ req: req as any, secret }) + + const authorized = options?.authorized ?? (({ token }) => token) + const isAuthorized = await authorized({ req, token }) + // the user is authorized, let the middleware handle the rest + if (isAuthorized) return await onSuccess?.(token) + + // the user is not logged in, re-direct to the sign-in page + return NextResponse.redirect( + `${signInPage}?${new URLSearchParams({ callbackUrl: req.url })}` + ) } +export type WithAuthArgs = + | [NextRequest] + | [NextRequest, NextFetchEvent] + | [NextRequest, NextAuthMiddlewareOptions] + | [NextMiddleware] + | [NextMiddleware, NextAuthMiddlewareOptions] + | [NextAuthMiddlewareOptions] + /** * Middleware that checks if the user is authenticated/authorized. * If if they aren't, they will be redirected to the login page. @@ -115,60 +117,23 @@ function initConfig( */ export function withAuth(...args: WithAuthArgs) { if (args[0] instanceof NextRequest) { - const config = initConfig(args[0], args[1]) - if (!config) return - if (config.redirect) return config.redirect - - const { req, secret, signInPage, authorized } = config - - return getToken({ req: req as any, secret }).then(async (token) => { - if (await authorized({ req, token })) return - - return NextResponse.redirect( - `${signInPage}?${new URLSearchParams({ callbackUrl: req.url })}` - ) - }) + // @ts-expect-error + return handleMiddleware(...args) } if (typeof args[0] === "function") { const middleware = args[0] const options = args[1] as NextAuthMiddlewareOptions | undefined - return async (...args: Parameters) => { - const config = initConfig(args[0], options) - if (!config) return - if (config.redirect) return config.redirect - - const { req, secret, signInPage, authorized } = config - - const token = await getToken({ req: req as any, secret }) - - if (await authorized({ req, token })) { + return async (...args: Parameters) => + await handleMiddleware(args[0], options, async (token) => { ;(args[0] as any).token = token return await middleware(...args) - } - - return NextResponse.redirect( - `${signInPage}?${new URLSearchParams({ callbackUrl: req.url })}` - ) - } + }) } const options = args[0] - return async (...args: Parameters) => { - const config = initConfig(args[0], options) - if (!config) return - if (config.redirect) return config.redirect - - const { req, secret, signInPage, authorized } = config - - const token = await getToken({ req: req as any, secret }) - - if (await authorized({ req, token })) return - - return NextResponse.redirect( - `${signInPage}?${new URLSearchParams({ callbackUrl: req.url })}` - ) - } + return async (...args: Parameters) => + await handleMiddleware(args[0], options) } export default withAuth From 5db7cc7c9a9b8f6ace917de89c42f10c25358996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 2 Feb 2022 16:30:16 +0100 Subject: [PATCH 19/22] refactor: use `callbacks` similarily to `NextAuthOptions` --- app/pages/middleware-protected/_middleware.js | 12 +++- src/next/middleware.ts | 62 +++++++++++-------- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/app/pages/middleware-protected/_middleware.js b/app/pages/middleware-protected/_middleware.js index 4f39389dd0..cf07b17d2d 100644 --- a/app/pages/middleware-protected/_middleware.js +++ b/app/pages/middleware-protected/_middleware.js @@ -16,7 +16,9 @@ export { default } from "next-auth/middleware" // export function middleware(req, ev) { // return withAuth(req, { // secret: process.env.NEXTAUTH_SECRET, -// authorized: ({ token }) => !!token, +// callbacks: { +// authorized: ({ token }) => !!token, +// }, // }) // } @@ -31,11 +33,15 @@ export { default } from "next-auth/middleware" // }, // { // secret: process.env.NEXTAUTH_SECRET, -// authorized: ({ token }) => token.name === "Balázs Orbán", +// callbacks: { +// authorized: ({ token }) => token.name === "Balázs Orbán", +// } // } // ) // export default withAuth({ // secret: process.env.NEXTAUTH_SECRET, -// authorized: ({ token }) => !!token, +// callbacks: { +// authorized: ({ token }) => !!token, +// }, // }) diff --git a/src/next/middleware.ts b/src/next/middleware.ts index d922c46f49..f78e9e11cb 100644 --- a/src/next/middleware.ts +++ b/src/next/middleware.ts @@ -7,6 +7,11 @@ import { NextResponse, NextRequest } from "next/server" import { getToken } from "../jwt" import parseUrl from "../lib/parse-url" +type AuthorizedCallback = (params: { + token: JWT | null + req: NextRequest +}) => Awaitable + export interface NextAuthMiddlewareOptions { /** * The secret used to create the session. @@ -16,7 +21,7 @@ export interface NextAuthMiddlewareOptions { * --- * [Documentation](https://next-auth.js.org/configuration/options#secret) */ - secret?: string + secret?: NextAuthOptions["secret"] /** * Where to redirect the user in case of an error if they weren't logged in. * Similar to `pages` in `NextAuth`. @@ -25,30 +30,34 @@ export interface NextAuthMiddlewareOptions { * [Documentation](https://next-auth.js.org/configuration/pages) */ pages?: NextAuthOptions["pages"] - /** - * Callback that receives the user's JWT payload - * and returns `true` to allow the user to continue. - * - * If it returns `false`, the user is redirected to the sign-in page instead - * - * The default is to let the user continue if they have a valid JWT (basic authentication). - * - * How to restrict a page and all of it's subpages for admins-only: - * @example - * - * ```js - * // `pages/admin/_middleware.js` - * import { withAuth } from "next-auth/next/middleware" - * - * export default withAuth({ - * authorized: ({ token }) => token?.user.isAdmin - * }) - * ``` - */ - authorized?: (options: { - token: JWT | null - req: NextRequest - }) => Awaitable + callbacks?: { + /** + * Callback that receives the user's JWT payload + * and returns `true` to allow the user to continue. + * + * This is similar to the `signIn` callback in `NextAuthOptions`. + * + * If it returns `false`, the user is redirected to the sign-in page instead + * + * The default is to let the user continue if they have a valid JWT (basic authentication). + * + * How to restrict a page and all of it's subpages for admins-only: + * @example + * + * ```js + * // `pages/admin/_middleware.js` + * import { withAuth } from "next-auth/next/middleware" + * + * export default withAuth({ + * authorized: ({ token }) => token?.user.isAdmin + * }) + * ``` + * + * --- + * [Documentation](https://next-auth.js.org/getting-started/nextjs/middleware#api) | [`signIn` callback](configuration/callbacks#sign-in-callback) + */ + authorized?: AuthorizedCallback + } } async function handleMiddleware( @@ -81,7 +90,8 @@ async function handleMiddleware( const token = await getToken({ req: req as any, secret }) - const authorized = options?.authorized ?? (({ token }) => token) + const authorized: AuthorizedCallback = + options?.callbacks?.authorized ?? (({ token }) => !!token) const isAuthorized = await authorized({ req, token }) // the user is authorized, let the middleware handle the rest if (isAuthorized) return await onSuccess?.(token) From f28ad9f4aaa619adda3484fa6d393dea5e1fba50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 2 Feb 2022 18:10:11 +0100 Subject: [PATCH 20/22] refactor: use `nextauth` namespace when setting `token` on `req` --- src/next/middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/next/middleware.ts b/src/next/middleware.ts index f78e9e11cb..14588a29b6 100644 --- a/src/next/middleware.ts +++ b/src/next/middleware.ts @@ -136,7 +136,7 @@ export function withAuth(...args: WithAuthArgs) { const options = args[1] as NextAuthMiddlewareOptions | undefined return async (...args: Parameters) => await handleMiddleware(args[0], options, async (token) => { - ;(args[0] as any).token = token + ;(args[0] as any).nextauth = { token } return await middleware(...args) }) } From 1b99fbabd962c5ac45dadbfa179bae3ef5a3d7c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Thu, 3 Feb 2022 17:26:11 +0100 Subject: [PATCH 21/22] refactor: don't allow passing `secret` --- app/pages/middleware-protected/_middleware.js | 3 --- src/next/middleware.ts | 17 +++++------------ 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/app/pages/middleware-protected/_middleware.js b/app/pages/middleware-protected/_middleware.js index cf07b17d2d..1ba18558f5 100644 --- a/app/pages/middleware-protected/_middleware.js +++ b/app/pages/middleware-protected/_middleware.js @@ -15,7 +15,6 @@ export { default } from "next-auth/middleware" // export function middleware(req, ev) { // return withAuth(req, { -// secret: process.env.NEXTAUTH_SECRET, // callbacks: { // authorized: ({ token }) => !!token, // }, @@ -32,7 +31,6 @@ export { default } from "next-auth/middleware" // return undefined // NOTE: `NextMiddleware` should allow returning `void` // }, // { -// secret: process.env.NEXTAUTH_SECRET, // callbacks: { // authorized: ({ token }) => token.name === "Balázs Orbán", // } @@ -40,7 +38,6 @@ export { default } from "next-auth/middleware" // ) // export default withAuth({ -// secret: process.env.NEXTAUTH_SECRET, // callbacks: { // authorized: ({ token }) => !!token, // }, diff --git a/src/next/middleware.ts b/src/next/middleware.ts index 14588a29b6..874b6002cf 100644 --- a/src/next/middleware.ts +++ b/src/next/middleware.ts @@ -13,15 +13,6 @@ type AuthorizedCallback = (params: { }) => Awaitable export interface NextAuthMiddlewareOptions { - /** - * The secret used to create the session. - * @note Must match as `secret` in `NextAuth`. - * @default process.env.NEXTAUTH_SECRET - * - * --- - * [Documentation](https://next-auth.js.org/configuration/options#secret) - */ - secret?: NextAuthOptions["secret"] /** * Where to redirect the user in case of an error if they weren't logged in. * Similar to `pages` in `NextAuth`. @@ -46,10 +37,12 @@ export interface NextAuthMiddlewareOptions { * * ```js * // `pages/admin/_middleware.js` - * import { withAuth } from "next-auth/next/middleware" + * import { withAuth } from "next-auth/middleware" * * export default withAuth({ - * authorized: ({ token }) => token?.user.isAdmin + * callbacks: { + * authorized: ({ token }) => token?.user.isAdmin + * } * }) * ``` * @@ -76,7 +69,7 @@ async function handleMiddleware( return } - const secret = options?.secret ?? process.env.NEXTAUTH_SECRET + const secret = process.env.NEXTAUTH_SECRET if (!secret) { console.error( `[next-auth][error][NO_SECRET]`, From 4a95e1cbba7bbdbf1c2883e270e331c142203423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Thu, 3 Feb 2022 17:58:36 +0100 Subject: [PATCH 22/22] addressing review --- app/pages/middleware-protected/_middleware.js | 2 +- src/next/middleware.ts | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/pages/middleware-protected/_middleware.js b/app/pages/middleware-protected/_middleware.js index 1ba18558f5..0447256e46 100644 --- a/app/pages/middleware-protected/_middleware.js +++ b/app/pages/middleware-protected/_middleware.js @@ -22,7 +22,7 @@ export { default } from "next-auth/middleware" // } // export default withAuth(function middleware(req, ev) { -// console.log(req.token) +// console.log(req.nextauth.token) // }) // export default withAuth( diff --git a/src/next/middleware.ts b/src/next/middleware.ts index 874b6002cf..d962753f92 100644 --- a/src/next/middleware.ts +++ b/src/next/middleware.ts @@ -69,8 +69,7 @@ async function handleMiddleware( return } - const secret = process.env.NEXTAUTH_SECRET - if (!secret) { + if (!process.env.NEXTAUTH_SECRET) { console.error( `[next-auth][error][NO_SECRET]`, `\nhttps://next-auth.js.org/errors#no_secret` @@ -81,11 +80,11 @@ async function handleMiddleware( } } - const token = await getToken({ req: req as any, secret }) + const token = await getToken({ req: req as any }) + + const isAuthorized = + (await options?.callbacks?.authorized?.({ req, token })) ?? !!token - const authorized: AuthorizedCallback = - options?.callbacks?.authorized ?? (({ token }) => !!token) - const isAuthorized = await authorized({ req, token }) // the user is authorized, let the middleware handle the rest if (isAuthorized) return await onSuccess?.(token)