|
| 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