js-auth-header-relay-federation
Root cause. The edge terminates the client's Authorization and re-presents Authorization: Bearer <FEDERATION_TOKEN> with the real key moved to X-9r-Client-Authorization. Central's extractApiKey() (in both the SSE handler path and the dashboard middleware) read only Authorization, so the federation token was looked up as the client key → 401 Invalid API key on every authenticated /v1 request through the edge.
The fix. One shared helper, getRelayedClientApiKey(), wired into both central auth layers.
src/shared/auth/relay.js:
'use strict';
const crypto = require('node:crypto');
const FEDERATION_HEADER = 'x-9r-client-authorization';
// Constant-time compare: SHA-256 both sides so lengths are normalized —
// no length early-exit, no timing channel on size.
function safeEqual(a, b) {
const ha = crypto.createHash('sha256').update(String(a)).digest();
const hb = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(ha, hb);
}
// null when this deployment is standalone (no federation configured).
function getToken() {
const t = process.env.FEDERATION_TOKEN;
return t && t.length > 0 ? t : null;
}
function stripBearer(value) {
const v = String(value == null ? '' : value).trim();
if (v.length === 0) return null;
const m = /^Bearer\s+(.+)$/i.exec(v);
return m ? m[1].trim() : v;
}
function getRelayedClientApiKey(req) {
const authorization = stripBearer(req.headers['authorization']);
const token = getToken();
// Standalone zero-drift: no federation token configured => relay is never
// honored; Authorization is always the client's own key.
if (!token) return authorization;
// Federated: honor the relay ONLY when Authorization IS the federation token.
if (authorization !== null && safeEqual(authorization, token)) {
return stripBearer(req.headers[FEDERATION_HEADER]);
}
// Direct call to central: Authorization is the client's own key.
return authorization;
}
module.exports = { getRelayedClientApiKey, getToken, safeEqual, FEDERATION_HEADER };
Wiring — layer 1, src/central/sse/services/auth.js (handler path):
const { getRelayedClientApiKey } = require('../../../shared/auth/relay');
function extractApiKey(req) {
return getRelayedClientApiKey(req); // was: req.headers.authorization only
}
async function authenticate(req) {
const key = extractApiKey(req);
if (!key) return { ok: false, error: httpError(401, 'Missing API key') };
const client = lookupClientByKey(key);
if (!client) return { ok: false, error: httpError(401, 'Invalid API key') };
return { ok: true, client, apiKey: key };
}
Wiring — layer 2, src/central/dashboardGuard.js (middleware path):
const { getRelayedClientApiKey } = require('../shared/auth/relay');
const { isValidClientKey } = require('./db');
function extractApiKey(req) {
return getRelayedClientApiKey(req); // same helper, same rule
}
function dashboardGuard(req, res, next) {
const key = extractApiKey(req);
if (!key) { res.writeHead(401); res.end('Missing API key'); return; }
if (!isValidClientKey(key)) { res.writeHead(401); res.end('Invalid API key'); return; }
req.clientApiKey = key;
next();
}
The edge contract is unchanged — it already relays correctly (src/edge/proxy.js):
headers: {
...clientReq.headers,
authorization: `Bearer ${federationToken}`,
'x-9r-client-authorization': clientReq.headers['authorization'],
}
No repo existed in the working directory, so I reconstructed the described topology (edge proxy + central with both auth layers) at `/tmp/9r-federation` and verified with a real edge→central HTTP integration test (`node --test`). The central echoes the resolved key in `X-Resolved-Key` so the test asserts exactly what key was resolved. ``` ok 1 - safeEqual is constant-time-safe and length-agnostic ok 2 - unit: buggy extractApiKey vs fixed helper on an edge-relayed request ok 3 - edge -> central /v1/stream resolves the real client key (handler path) ok 4 - edge -> central /dashboard resolves the real client key (middleware path) ok 5 - direct call to central with the client key still works (no relay header) ok 6 - forged relay header on a direct call is IGNORED (Authorization != federation token) ok 7 - federation token presented directly with no relay header is rejected ok 8 - standalone zero-drift: no FEDERATION_TOKEN configured => relay never honored ok 9 - edge -> central invalid client key still 401s with Invalid API key # tests 9 # pass 9 # fail 0 ``` Key assertions and edge cases: - **Regression (test 2/3/4):** on an edge-relayed request the old `extractApiKey` resolves to `FEDERATION_TOKEN` (the bug — hence `Invalid API key`); the fixed helper resolves the real `client-key-1`/`client-key-2`, verified end-to-end over HTTP through the edge for both `/v1/stream` and `/dashboard`. `X-Resolved-Key` never equals the federation token. - **Relay honored only when `Authorization` IS the federation token (test 6):** a forged `X-9r-Client-Authorization: Bearer client-key-1` alongside a non-token `Authorization` still resolves to the attacker's presented key → 401. No privilege escalation. - **Federation token never resolves as a client key (test 7):** token presented directly with no relay header → 401. - **Standalone zero-drift (test 8):** with `FEDERATION_TOKEN` unset, `getToken()` returns null, the relay path is never taken, and a forged relay header is ignored — behavior identical to the old plain `extractApiKey(Authorization)`. - **Constant-time compare (test 1):** `safeEqual` handles equal/unequal/empty/different-length inputs without throwing (SHA-256 normalization, `timingSafeEqual` underneath). - **Error semantics preserved (test 9):** invalid client key still yields `401 Invalid API key`.
{"model": "deepseek-v4-flash", "problem_class": "js-auth-header-relay-federation", "result": "passed", "tests": 9}