Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/next-auth/src/lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { ProviderId, ProviderType } from "@auth/core/providers"
import type { LoggerInstance, Session } from "@auth/core/types"
import { AuthError } from "@auth/core/errors"

import { getSkewProtectionHeaderInit } from "./vercel-skew-protection.js"

/** @todo */
class ClientFetchError extends AuthError {}

Expand Down Expand Up @@ -148,6 +150,7 @@ export async function fetchData<T = any>(
headers: {
"Content-Type": "application/json",
...(req?.headers?.cookie ? { cookie: req.headers.cookie } : {}),
...getSkewProtectionHeaderInit(),
},
}

Expand Down
39 changes: 39 additions & 0 deletions packages/next-auth/src/lib/vercel-skew-protection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { afterEach, describe, expect, it, vi } from "vitest"

import {
getSkewProtectionHeaderInit,
resolveDeploymentIdForSkewProtection,
} from "./vercel-skew-protection.js"

afterEach(() => {
vi.unstubAllEnvs()
;(globalThis as Record<string, unknown>).NEXT_DEPLOYMENT_ID = undefined
})

describe("resolveDeploymentIdForSkewProtection", () => {
it("prefers globalThis.NEXT_DEPLOYMENT_ID", () => {
;(globalThis as Record<string, unknown>).NEXT_DEPLOYMENT_ID = "dpl_global"
expect(resolveDeploymentIdForSkewProtection()).toBe("dpl_global")
})

it("uses VERCEL_DEPLOYMENT_ID when global is unset", () => {
vi.stubEnv("VERCEL_DEPLOYMENT_ID", "dpl_env")
expect(resolveDeploymentIdForSkewProtection()).toBe("dpl_env")
})
})

describe("getSkewProtectionHeaderInit", () => {
it("returns empty when skew protection is disabled", () => {
vi.stubEnv("VERCEL_SKEW_PROTECTION_ENABLED", "0")
vi.stubEnv("VERCEL_DEPLOYMENT_ID", "dpl_x")
expect(getSkewProtectionHeaderInit()).toEqual({})
})

it("returns x-deployment-id when enabled and id is resolvable", () => {
vi.stubEnv("VERCEL_SKEW_PROTECTION_ENABLED", "1")
vi.stubEnv("VERCEL_DEPLOYMENT_ID", "dpl_abc")
expect(getSkewProtectionHeaderInit()).toEqual({
"x-deployment-id": "dpl_abc",
})
})
})
50 changes: 50 additions & 0 deletions packages/next-auth/src/lib/vercel-skew-protection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* [Vercel Skew Protection](https://vercel.com/docs/skew-protection) does not automatically
* pin custom `fetch()` calls. Next.js sets `globalThis.NEXT_DEPLOYMENT_ID` from `data-dpl-id`
* on the document; we also fall back to Vercel/Next deployment id env vars when available.
*/

const DEPLOYMENT_ID_HEADER = "x-deployment-id"

function isSkewProtectionEnabled(): boolean {
if (typeof process === "undefined") return true
const flag = process.env.VERCEL_SKEW_PROTECTION_ENABLED
return flag === undefined || flag === "1"
}

export function resolveDeploymentIdForSkewProtection(): string | undefined {
if (typeof globalThis !== "undefined") {
const fromGlobal = (globalThis as Record<string, unknown>)
.NEXT_DEPLOYMENT_ID
if (typeof fromGlobal === "string" && fromGlobal.length > 0) {
return fromGlobal
}
}
if (typeof document !== "undefined") {
const fromDataset = document.documentElement?.dataset?.dplId
if (fromDataset) return fromDataset
const fromAttr =
document.documentElement?.getAttribute("data-dpl-id") ?? undefined
if (fromAttr) return fromAttr
}
if (typeof process !== "undefined") {
if (process.env.VERCEL_DEPLOYMENT_ID) {
return process.env.VERCEL_DEPLOYMENT_ID
}
if (process.env.NEXT_DEPLOYMENT_ID) {
return process.env.NEXT_DEPLOYMENT_ID
}
}
return undefined
}

/**
* Header entries to merge into client-side Auth.js fetches so they stay pinned during
* rolling releases on Vercel.
*/
export function getSkewProtectionHeaderInit(): Record<string, string> {
if (!isSkewProtectionEnabled()) return {}
const deploymentId = resolveDeploymentIdForSkewProtection()
if (!deploymentId) return {}
return { [DEPLOYMENT_ID_HEADER]: deploymentId }
}
3 changes: 3 additions & 0 deletions packages/next-auth/src/react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
parseUrl,
useOnline,
} from "./lib/client.js"
import { getSkewProtectionHeaderInit } from "./lib/vercel-skew-protection.js"

import type { ProviderId } from "@auth/core/providers"
import type { LoggerInstance, Session } from "@auth/core/types"
Expand Down Expand Up @@ -288,6 +289,7 @@ export async function signIn<Redirect extends boolean = true>(
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"X-Auth-Return-Redirect": "1",
...getSkewProtectionHeaderInit(),
},
body: new URLSearchParams({
...signInParams,
Expand Down Expand Up @@ -349,6 +351,7 @@ export async function signOut<R extends boolean = true>(
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"X-Auth-Return-Redirect": "1",
...getSkewProtectionHeaderInit(),
},
body: new URLSearchParams({ csrfToken, callbackUrl: redirectTo }),
})
Expand Down
5 changes: 4 additions & 1 deletion packages/next-auth/src/webauthn.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { apiBaseUrl } from "./lib/client.js"
import { getSkewProtectionHeaderInit } from "./lib/vercel-skew-protection.js"
import { startAuthentication, startRegistration } from "@simplewebauthn/browser"
import { getCsrfToken, getProviders, __NEXTAUTH } from "./react.js"

Expand Down Expand Up @@ -33,7 +34,8 @@ async function webAuthnOptions(
const params = new URLSearchParams(options)

const optionsResp = await fetch(
`${baseUrl}/webauthn-options/${providerID}?${params}`
`${baseUrl}/webauthn-options/${providerID}?${params}`,
{ headers: { ...getSkewProtectionHeaderInit() } }
)
if (!optionsResp.ok) {
return { error: optionsResp }
Expand Down Expand Up @@ -116,6 +118,7 @@ export async function signIn<Redirect extends boolean = true>(
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"X-Auth-Return-Redirect": "1",
...getSkewProtectionHeaderInit(),
},
body: new URLSearchParams({
...signInParams,
Expand Down
Loading