/** * MWT / JWT decode — HS256 verification, cross-language parity with * cores/mizan-python/src/mizan_core/mwt.py. * * Returns null on ANY failure (bad signature, expired, future nbf, wrong * aud, malformed). Never throws. */ import { createHmac, timingSafeEqual } from 'crypto' import type { Identity } from './identity' export interface MwtPayload { sub: string staff: boolean super: boolean pkey: string kid: string aud: string iat: number exp: number } function base64urlDecode(input: string): Buffer | null { if (!/^[A-Za-z0-9_-]*$/.test(input)) return null return Buffer.from(input, 'base64url') } function constantTimeEqual(a: Buffer, b: Buffer): boolean { if (a.length !== b.length) return false return timingSafeEqual(a, b) } /** * Decode and validate an MWT (HS256 JWT with Mizan claims). * Returns MwtPayload on success, null on any failure. */ export function decodeMwt( token: string, secret: string, audience: string = 'mizan', ): MwtPayload | null { try { const parts = token.split('.') if (parts.length !== 3) return null const [headerB64, payloadB64, signatureB64] = parts const headerBytes = base64urlDecode(headerB64) const payloadBytes = base64urlDecode(payloadB64) const signatureBytes = base64urlDecode(signatureB64) if (!headerBytes || !payloadBytes || !signatureBytes) return null const header = JSON.parse(headerBytes.toString('utf-8')) if (header.alg !== 'HS256') return null // Recompute HMAC over `${headerB64}.${payloadB64}` const expected = createHmac('sha256', secret) .update(`${headerB64}.${payloadB64}`) .digest() if (!constantTimeEqual(expected, signatureBytes)) return null const data = JSON.parse(payloadBytes.toString('utf-8')) const now = Math.floor(Date.now() / 1000) if (typeof data.exp !== 'number' || data.exp <= now) return null if (data.nbf !== undefined && typeof data.nbf === 'number' && data.nbf > now) return null if (data.aud !== audience) return null const kid = typeof header.kid === 'string' ? header.kid : 'v1' return { sub: String(data.sub), staff: Boolean(data.staff), super: Boolean(data.super), pkey: typeof data.pkey === 'string' ? data.pkey : '', kid, aud: audience, iat: data.iat, exp: data.exp, } } catch { return null } } /** * Decode a Bearer JWT from an Authorization header value. * Strips the "Bearer " prefix, then validates as an MWT. */ export function decodeJwtBearer( authHeader: string, secret: string, audience: string = 'mizan', ): MwtPayload | null { if (!authHeader) return null const prefix = 'Bearer ' const token = authHeader.startsWith(prefix) ? authHeader.slice(prefix.length) : authHeader return decodeMwt(token, secret, audience) } /** Build an Identity from a decoded MWT payload. */ export function identityFromMwt(payload: MwtPayload): Identity { return { isAuthenticated: true, isStaff: payload.staff, isSuperuser: payload.super, id: Number(payload.sub), } }