verae-middleware/src/lib/tokens.js

97 lines
2.5 KiB
JavaScript

/**
* @fileoverview API keys and HMAC session tokens for the middleware auth bridge.
* @module lib/tokens
*/
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
import { config } from '../config.js';
const TOKEN_PREFIX = 'zmt_';
const API_KEY_PREFIX = 'zmw_';
/**
* @param {string} payload
* @returns {string}
*/
function sign(payload) {
return createHmac('sha256', config.tokenSecret).update(payload).digest('base64url');
}
/**
* Issue a signed middleware session token embedding tenant + Verae JWT.
*
* @param {object} params
* @param {string} params.tenantId
* @param {string} params.veraeToken
* @param {string} [params.expiresAt]
* @returns {string} Token string starting with `zmt_`
*/
export function issueSessionToken({ tenantId, veraeToken, expiresAt }) {
const payload = Buffer.from(
JSON.stringify({
tenantId,
veraeToken,
expiresAt,
nonce: randomBytes(8).toString('hex'),
}),
).toString('base64url');
const signature = sign(payload);
return `${TOKEN_PREFIX}${payload}.${signature}`;
}
/**
* Parse and verify a session token.
*
* @param {string} token
* @returns {{ tenantId: string, veraeToken: string, expiresAt?: string, nonce?: string }|null}
*/
export function parseSessionToken(token) {
if (!token?.startsWith(TOKEN_PREFIX)) return null;
const raw = token.slice(TOKEN_PREFIX.length);
const dot = raw.lastIndexOf('.');
if (dot === -1) return null;
const payload = raw.slice(0, dot);
const signature = raw.slice(dot + 1);
const expected = sign(payload);
const sigBuf = Buffer.from(signature);
const expBuf = Buffer.from(expected);
if (sigBuf.length !== expBuf.length || !timingSafeEqual(sigBuf, expBuf)) {
return null;
}
try {
return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
} catch {
return null;
}
}
/**
* Generate a new public API key (`zmw_…`).
* @returns {string}
*/
export function generateApiKey() {
return `${API_KEY_PREFIX}${randomBytes(24).toString('base64url')}`;
}
/**
* @param {unknown} value
* @returns {boolean}
*/
export function isApiKey(value) {
return typeof value === 'string' && value.startsWith(API_KEY_PREFIX);
}
/**
* Extract bearer credential from an Authorization header.
* @param {string|undefined} header
* @returns {string|null}
*/
export function extractBearerToken(header) {
if (!header) return null;
const match = String(header).match(/^Bearer\s+(.+)$/i);
return match?.[1] ?? null;
}