61 lines
2.2 KiB
TypeScript
61 lines
2.2 KiB
TypeScript
// 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
|
||
}
|
||
}
|