Files
web/middlewares/authRequired.ts
2024-08-22 07:25:22 +02:00

102 lines
3.3 KiB
TypeScript

import { NextResponse } from "next/server"
import { authRequired, mfaRequired } from "@/constants/routes/authRequired"
import { login } from "@/constants/routes/handleAuth"
import { env } from "@/env/server"
import { internalServerError } from "@/server/errors/next"
import { auth } from "@/auth"
import { findLang } from "@/utils/languages"
import type { NextMiddleware } from "next/server"
import type { MiddlewareMatcher } from "@/types/middleware"
/**
* AppRouteHandlerFnContext is the context that is passed to the handler as
* the second argument. This is only done for Route handlers (route.js) and
* not for middleware. Middleware`s second argument is `event` of type
* `NextFetchEvent`.
*
* Auth.js uses the same pattern for both Route handlers and Middleware,
* the auth()-wrapper:
*
* auth((req) => { ... })
*
* But there is a difference between middleware and route handlers, route
* handlers get passed a context which middleware do not get (they get a
* NextFetchEvent instead). Using the same function for both works runtime
* because Auth.js handles this properly. But fails in typings as the second
* argument doesn't match for middleware.
*
* We want to avoid using ts-expect-error because that hides other errors
* not related to this typing error and ts-expect-error cannot be scoped either.
*
* So we type assert this export to NextMiddleware. The lesser of all evils.
*
* https://github.com/nextauthjs/next-auth/blob/3c035ec62f2f21d7cab65504ba83fb1a9a13be01/packages/next-auth/src/lib/index.ts#L265
* https://authjs.dev/reference/nextjs
*/
export const middleware = auth(async (request) => {
const { nextUrl } = request
const lang = findLang(nextUrl.pathname)!
const isLoggedIn = !!request.auth
const hasError = request.auth?.error
if (!env.PUBLIC_URL) {
throw internalServerError("Missing value for env.PUBLIC_URL")
}
const publicUrl = new URL(env.PUBLIC_URL)
const nextUrlClone = nextUrl.clone()
nextUrlClone.host = publicUrl.host
nextUrlClone.hostname = publicUrl.hostname
/**
* Function to validate MFA from token data
* @returns boolean
*/
function isMFAInvalid() {
const isMFATokenValid = request.auth
? request.auth.token.mfa_expires_at > Date.now()
: false
return !(request.auth?.token.mfa_scope && isMFATokenValid)
}
const isMFAPath = mfaRequired.includes(nextUrl.pathname)
if (isLoggedIn && isMFAPath && isMFAInvalid()) {
const headers = new Headers(request.headers)
headers.set("x-returnurl", nextUrlClone.href)
headers.set("x-login-source", "mfa")
return NextResponse.rewrite(new URL(`/${lang}/login`, request.nextUrl), {
request: {
headers,
},
})
}
if (isLoggedIn && !hasError) {
const headers = new Headers(request.headers)
headers.set("x-continue", "1")
return NextResponse.next({
headers,
})
}
const headers = new Headers()
headers.append(
"set-cookie",
`redirectTo=${encodeURIComponent(nextUrlClone.href)}; Path=/; HttpOnly; SameSite=Lax`
)
const loginUrl = login[lang]
return NextResponse.redirect(new URL(loginUrl, nextUrlClone), {
headers,
})
}) as NextMiddleware // See comment above
export const matcher: MiddlewareMatcher = (request) => {
return authRequired.includes(request.nextUrl.pathname)
}