gongxiangp2/lib/dingtalk-return-origin.ts
2026-07-17 20:16:05 +08:00

90 lines
3.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const DEFAULT_DINGTALK_RETURN_ORIGINS = ['http://192.168.58.20:4320']
type DingtalkRequestLike = {
headers?: {
get?: (name: string) => string | null | undefined
}
nextUrl?: {
origin?: string
}
}
function normalizeHttpOrigin(value: unknown): string | null {
const raw = String(value ?? '').trim()
if (!raw) return null
try {
const parsed = new URL(raw)
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return null
}
if (parsed.username || parsed.password || parsed.pathname !== '/' || parsed.search || parsed.hash) {
return null
}
return parsed.origin
} catch {
return null
}
}
function parseConfiguredOrigins(value: string | undefined): string[] {
return String(value || '')
.split(',')
.map((item) => normalizeHttpOrigin(item))
.filter((item): item is string => Boolean(item))
}
function getFirstHeaderValue(request: DingtalkRequestLike, name: string): string {
const raw = request.headers?.get?.(name) || ''
return raw.split(',')[0].trim()
}
function getProtocolFromForwardedHeader(request: DingtalkRequestLike): 'http' | 'https' | null {
const protocol = getFirstHeaderValue(request, 'x-forwarded-proto').replace(/:$/, '').toLowerCase()
if (protocol === 'http' || protocol === 'https') {
return protocol
}
return null
}
/**
* 钉钉必须回到公网完成 OAuth;这里仅定义 OAuth 完成后允许交接会话的内网入口。
*
* 生产环境通过 DINGTALK_INTERNAL_RETURN_ORIGINS 显式配置。未配置时仅保留当前稳定内网入口,
* 防止把 state 中任意地址当成跳转目标。
*/
export function getDingtalkReturnOrigins(): string[] {
const configured = parseConfiguredOrigins(process.env.DINGTALK_INTERNAL_RETURN_ORIGINS)
const candidates = configured.length > 0 ? configured : DEFAULT_DINGTALK_RETURN_ORIGINS
return Array.from(new Set(candidates))
}
export function getTrustedDingtalkReturnOrigin(value: unknown): string | null {
const origin = normalizeHttpOrigin(value)
if (!origin) return null
return getDingtalkReturnOrigins().includes(origin) ? origin : null
}
/**
* Next.js 在设置 NEXTAUTH_URL 后可能把 request.nextUrl 固定解析为公网地址。
* 内网交接必须优先使用反向代理实际传入的 Host + Protocol,再做精确白名单校验。
*/
export function getTrustedDingtalkReturnOriginFromRequest(request: DingtalkRequestLike): string | null {
// 本地代理明确保留 Host;X-Forwarded-Host 只作为没有 Host 时的兼容兜底,
// 避免把客户端可伪造的转发头当成首选来源。
const host = getFirstHeaderValue(request, 'host') || getFirstHeaderValue(request, 'x-forwarded-host')
if (host) {
const protocol = getProtocolFromForwardedHeader(request) || 'http'
const trustedOrigin = getTrustedDingtalkReturnOrigin(`${protocol}://${host}`)
if (trustedOrigin) {
return trustedOrigin
}
}
return getTrustedDingtalkReturnOrigin(request.nextUrl?.origin)
}