// shared/lib/auth/jwt-v2.ts — V2 JWT 签发/验证(步骤 1a 新增,旧 jwt.ts 不动) // 与 V1 的关键差异:signJwtV2 强制注入 iss,verifyJwtV2 强制校验 iss 匹配 import crypto from 'crypto' import type { SessionPayloadV2 } from './types-v2' interface SignJwtV2Options { secret: string payload: { username: string; displayName?: string } /** 签发者标识:OA LDAP 用 "oa.tlyq.ai",OIDC 用 autheliaUrl */ iss: string expiresInSeconds?: number } /** 签发 JWT(V2),强制注入 iss */ export function signJwtV2({ secret, payload, iss, expiresInSeconds = 604800 }: SignJwtV2Options): string { const header = { alg: 'HS256', typ: 'JWT' } const now = Math.floor(Date.now() / 1000) const body = { ...payload, iss, iat: now, exp: now + expiresInSeconds } const encoded = (obj: object) => Buffer.from(JSON.stringify(obj)).toString('base64url') const signature = crypto .createHmac('sha256', secret) .update(`${encoded(header)}.${encoded(body)}`) .digest('base64url') return `${encoded(header)}.${encoded(body)}.${signature}` } /** 验证 JWT(V2),3 参数强制校验 iss 匹配 */ export function verifyJwtV2(token: string, secret: string, expectedIss: string): SessionPayloadV2 | null { try { const parts = token.split('.') if (parts.length !== 3) return null const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString()) as SessionPayloadV2 if (payload.exp && payload.exp * 1000 < Date.now()) return null if (payload.iss !== expectedIss) return null const signature = crypto .createHmac('sha256', secret) .update(`${parts[0]}.${parts[1]}`) .digest('base64url') return signature === parts[2] ? payload : null } catch { return null } } /** 从 token 解码提取 iss(不验签,用于 Layer 2/3 自引用校验:先提取 iss → 再 verifyJwtV2) */ export function extractIss(token: string): string | null { try { const parts = token.split('.') if (parts.length !== 3) return null const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString()) as SessionPayloadV2 return payload.iss || null } catch { return null } }