Files
web/middlewares/authRequired.ts
Michael Zetterberg 4a846540c3 feat: improve handling of deployment env vars
These are now defined in Netlify UI for dedicated environments (test, stage, production):

AUTH_URL
NEXTAUTH_URL
PUBLIC_URL

Code now falls back to incoming request host. Mainly used for
deployment previews which do not have Akamai in front, meaning
we do not need the above workaround as incoming request host
matches the actual public facing host. When Akamai is in front,
we lose the public facing host in Netlify's routing layer as they
internally use `x-forwarded-for` and we can't claim it for our usage.
2024-10-15 17:03:36 +02:00

97 lines
3.2 KiB
TypeScript

import { NextResponse } from "next/server"
import { authRequired, mfaRequired } from "@/constants/routes/authRequired"
import { login } from "@/constants/routes/handleAuth"
import { getPublicNextURL } from "@/server/utils"
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
const nextUrlPublic = getPublicNextURL(request)
/**
* 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", nextUrlPublic.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(nextUrlPublic.href)}; Path=/; HttpOnly; SameSite=Lax`
)
const loginUrl = login[lang]
const redirectUrl = new URL(loginUrl, nextUrlPublic)
const redirectOpts = {
headers,
}
console.log(`[authRequired] redirecting to: ${redirectUrl}`, redirectOpts)
return NextResponse.redirect(redirectUrl, redirectOpts)
}) as NextMiddleware // See comment above
export const matcher: MiddlewareMatcher = (request) => {
return authRequired.includes(request.nextUrl.pathname)
}