feat: add utility for getting internal next url
This commit is contained in:
@@ -15,6 +15,10 @@ import * as webView from "@/middlewares/webView"
|
||||
import { findLang } from "@/utils/languages"
|
||||
|
||||
export const middleware: NextMiddleware = async (request, event) => {
|
||||
// auth() overrides the request origin, we need the original for internal rewrites
|
||||
// @see getInternalNextURL()
|
||||
request.headers.set("x-sh-origin", request.nextUrl.origin)
|
||||
|
||||
const headers = getDefaultRequestHeaders(request)
|
||||
const lang = findLang(request.nextUrl.pathname)
|
||||
|
||||
@@ -60,6 +64,8 @@ export const middleware: NextMiddleware = async (request, event) => {
|
||||
if (_continue) {
|
||||
continue
|
||||
}
|
||||
// Clean up internal headers
|
||||
result?.headers.delete("x-sh-origin")
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NextResponse } from "next/server"
|
||||
|
||||
import { authRequired, mfaRequired } from "@/constants/routes/authRequired"
|
||||
import { login } from "@/constants/routes/handleAuth"
|
||||
import { getPublicNextURL } from "@/server/utils"
|
||||
import { getInternalNextURL, getPublicNextURL } from "@/server/utils"
|
||||
|
||||
import { auth } from "@/auth"
|
||||
import { findLang } from "@/utils/languages"
|
||||
@@ -37,12 +37,15 @@ import type { MiddlewareMatcher } from "@/types/middleware"
|
||||
* https://authjs.dev/reference/nextjs
|
||||
*/
|
||||
export const middleware = auth(async (request) => {
|
||||
const { nextUrl } = request
|
||||
const lang = findLang(nextUrl.pathname)!
|
||||
const lang = findLang(request.nextUrl.pathname)!
|
||||
|
||||
const isLoggedIn = !!request.auth
|
||||
const hasError = request.auth?.error
|
||||
|
||||
// Inside auth() we need an internal request for rewrites.
|
||||
// @see getInternalNextURL()
|
||||
const nextUrlInternal = getInternalNextURL(request)
|
||||
|
||||
const nextUrlPublic = getPublicNextURL(request)
|
||||
|
||||
/**
|
||||
@@ -55,13 +58,13 @@ export const middleware = auth(async (request) => {
|
||||
: false
|
||||
return !(request.auth?.token.mfa_scope && isMFATokenValid)
|
||||
}
|
||||
const isMFAPath = mfaRequired.includes(nextUrl.pathname)
|
||||
const isMFAPath = mfaRequired.includes(request.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), {
|
||||
return NextResponse.rewrite(new URL(`/${lang}/login`, nextUrlInternal), {
|
||||
request: {
|
||||
headers,
|
||||
},
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { NextRequest } from "next/server"
|
||||
import { z } from "zod"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import type { NextRequest } from "next/server"
|
||||
|
||||
export const langInput = z.object({
|
||||
lang: z.nativeEnum(Lang),
|
||||
})
|
||||
@@ -37,16 +36,20 @@ export function toLang(lang: string): Lang | undefined {
|
||||
return Object.values(Lang).find((l) => l === lowerCaseLang)
|
||||
}
|
||||
|
||||
export function getPublicURL(request: NextRequest) {
|
||||
if (env.PUBLIC_URL) {
|
||||
return env.PUBLIC_URL
|
||||
}
|
||||
|
||||
const host = request.nextUrl.host
|
||||
const proto = request.nextUrl.protocol
|
||||
return `${proto}//${host}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this function when you want to create URLs that are public facing, for
|
||||
* example for redirects or redirectTo query parameters.
|
||||
* Dedicated environments are behind Akamai (test, stage, production). They have
|
||||
* env.PUBLIC_URL set.
|
||||
* All other environment like deploy previews and branch deployments are not
|
||||
* behind Akamai and therefore do not have env.PUBLIC_URL set.
|
||||
* We need this approach because Netlify uses x-forwarded-host internally and
|
||||
* strips it from ever reaching our code.
|
||||
* TODO: Replace this approach with custom header in Akamai that mirrors the
|
||||
* value in x-forwarded-host which would not get stripped by Netlify.
|
||||
* @param request The incoming request.
|
||||
* @returns NextURL The public facing URL instance for the given request.
|
||||
*/
|
||||
export function getPublicNextURL(request: NextRequest) {
|
||||
if (env.PUBLIC_URL) {
|
||||
const publicNextURL = request.nextUrl.clone()
|
||||
@@ -60,3 +63,65 @@ export function getPublicNextURL(request: NextRequest) {
|
||||
}
|
||||
return request.nextUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this function when you want the public facing URL for the given request.
|
||||
* Read about the motivation in getPublicNextURL above.
|
||||
* @see getPublicNextURL
|
||||
* @param request The incoming request.
|
||||
* @returns string The public facing origin for the given request.
|
||||
*/
|
||||
export function getPublicURL(request: NextRequest) {
|
||||
if (env.PUBLIC_URL) {
|
||||
return env.PUBLIC_URL
|
||||
}
|
||||
return request.nextUrl.origin
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this function when you want to create URLs that are internal (behind Akamai),
|
||||
* for example for rewrites. Mainly used for middleware wrapped in auth().
|
||||
* The auth() function overrides the origin of the incoming request. It sets it
|
||||
* to the origin of AUTH_URL/NEXTAUTH_URL. This means we cannot use the augmented
|
||||
* request from auth() for rewrites, as those will point to auth url origin
|
||||
* (in front of Akamai) and not the origin of the incoming request (behind Akamai).
|
||||
* This results in rewrites going over the internet instead of going through the
|
||||
* internal routing at Netlify.
|
||||
* For dedicated environments (test, stage and production) we are behind Akamai.
|
||||
* For those we have set a value for AUTH_URL/NEXTAUTH_URL, they point to the
|
||||
* PUBLIC_URL. For rewrites we need the internal origin inside Netlify.
|
||||
* In middleware.ts we copy the incoming origin to a header 'x-sh-origin'. We try
|
||||
* and use that first, if not present we assume the internal origin is the value
|
||||
* of the host header, as that is what Netlify used for routing to this deployment.
|
||||
* @param request The incoming request.
|
||||
* @returns NextURL The internal request, in Netlify behind Akamai.
|
||||
*/
|
||||
export function getInternalNextURL(request: NextRequest) {
|
||||
const { href, origin } = request.nextUrl
|
||||
|
||||
const originHeader = request.headers.get("x-sh-origin")
|
||||
if (originHeader) {
|
||||
console.log(`[internalNextUrl] using x-sh-origin header`, {
|
||||
origin,
|
||||
originHeader,
|
||||
newOrigin: href.replace(origin, originHeader),
|
||||
})
|
||||
return new NextRequest(href.replace(origin, originHeader), request).nextUrl
|
||||
}
|
||||
|
||||
const hostHeader = request.headers.get("host")
|
||||
if (hostHeader) {
|
||||
const inputHostOrigin = `${request.nextUrl.protocol}//${hostHeader}`
|
||||
console.log(`[internalNextUrl] using host header`, {
|
||||
origin,
|
||||
hostHeader,
|
||||
hostOrigin: inputHostOrigin,
|
||||
newOrigin: href.replace(origin, inputHostOrigin),
|
||||
})
|
||||
const { origin: hostOrigin } = new URL(inputHostOrigin)
|
||||
return new NextRequest(href.replace(origin, hostOrigin), request).nextUrl
|
||||
}
|
||||
|
||||
console.log(`[internalNextUrl] falling back to incoming request`)
|
||||
return request.nextUrl
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user