97 lines
2.5 KiB
JavaScript
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;
|
|
}
|