gongxiangp2/lib/auth-cookie.ts
2026-07-07 13:31:01 +08:00

166 lines
4.3 KiB
TypeScript

import { headers } from 'next/headers'
const AUTH_COOKIE_BASE_URL =
process.env.APP_PUBLIC_URL ||
process.env.NEXTAUTH_URL ||
''
export const SECURE_AUTH_SESSION_COOKIE_NAME = '__Secure-next-auth.session-token'
export const PLAIN_AUTH_SESSION_COOKIE_NAME = 'next-auth.session-token'
type HeaderAccessor =
| Headers
| {
get?: (name: string) => string | null | undefined
[key: string]: string | string[] | undefined | ((name: string) => string | null | undefined)
}
type RequestLike = {
headers?: HeaderAccessor
nextUrl?: {
protocol?: string
}
url?: string
}
function normalizeProtocol(value?: string | null) {
if (!value) {
return null
}
const normalized = value.split(',')[0].trim().replace(/:$/, '').toLowerCase()
if (normalized === 'https' || normalized === 'http') {
return normalized
}
return null
}
function getHeaderValue(headersLike: HeaderAccessor | undefined, name: string) {
if (!headersLike) {
return null
}
if (typeof headersLike.get === 'function') {
return headersLike.get(name) ?? null
}
const recordHeaders = headersLike as Record<string, string | string[] | undefined>
const directValue = recordHeaders[name]
if (Array.isArray(directValue)) {
return directValue[0] ?? null
}
if (typeof directValue === 'string') {
return directValue
}
const lowerCaseValue = recordHeaders[name.toLowerCase()]
if (Array.isArray(lowerCaseValue)) {
return lowerCaseValue[0] ?? null
}
return typeof lowerCaseValue === 'string' ? lowerCaseValue : null
}
function resolveProtocolFromHeaders(headersLike?: HeaderAccessor) {
const forwardedProto = normalizeProtocol(getHeaderValue(headersLike, 'x-forwarded-proto'))
if (forwardedProto) {
return forwardedProto
}
const forwardedSsl = getHeaderValue(headersLike, 'x-forwarded-ssl')
if (typeof forwardedSsl === 'string' && forwardedSsl.toLowerCase() === 'on') {
return 'https'
}
const forwardedPort = getHeaderValue(headersLike, 'x-forwarded-port')
if (forwardedPort === '443') {
return 'https'
}
return null
}
function resolveProtocolFromUrl(url?: string) {
if (!url) {
return null
}
try {
return normalizeProtocol(new URL(url).protocol)
} catch {
return null
}
}
function resolveSecureCookieFromPublicBase(headersLike?: HeaderAccessor, url?: string) {
if (!AUTH_COOKIE_BASE_URL.startsWith('https://')) {
return null
}
try {
const publicUrl = new URL(AUTH_COOKIE_BASE_URL)
const forwardedHost = getHeaderValue(headersLike, 'x-forwarded-host')
const host = forwardedHost || getHeaderValue(headersLike, 'host') || (url ? new URL(url).host : '')
if (!host) {
return null
}
const normalizedHost = host.split(',')[0].trim().toLowerCase()
return normalizedHost === publicUrl.host.toLowerCase() ? true : null
} catch {
return null
}
}
export function resolveSecureAuthCookieFromRequest(request?: RequestLike) {
const secureFromPublicBase = resolveSecureCookieFromPublicBase(request?.headers, request?.url)
if (secureFromPublicBase === true) {
return true
}
const protocolFromHeaders = resolveProtocolFromHeaders(request?.headers)
if (protocolFromHeaders) {
return protocolFromHeaders === 'https'
}
const protocolFromNextUrl = normalizeProtocol(request?.nextUrl?.protocol)
if (protocolFromNextUrl) {
return protocolFromNextUrl === 'https'
}
const protocolFromUrl = resolveProtocolFromUrl(request?.url)
if (protocolFromUrl) {
return protocolFromUrl === 'https'
}
return AUTH_COOKIE_BASE_URL.startsWith('https://')
}
export function resolveSecureAuthCookieForCurrentRequest() {
try {
return resolveSecureAuthCookieFromRequest({
headers: headers(),
})
} catch {
return AUTH_COOKIE_BASE_URL.startsWith('https://')
}
}
export function getAuthSessionCookieName(useSecureCookie: boolean) {
return useSecureCookie
? SECURE_AUTH_SESSION_COOKIE_NAME
: PLAIN_AUTH_SESSION_COOKIE_NAME
}
export function getAuthSessionCookieCandidates(preferredSecureCookie: boolean) {
const preferredName = getAuthSessionCookieName(preferredSecureCookie)
const fallbackName = getAuthSessionCookieName(!preferredSecureCookie)
return preferredName === fallbackName
? [preferredName]
: [preferredName, fallbackName]
}