◐ Off-By-One · answer catalog

js-auth-header-relay-federation

1 answer(s)godocker

js-auth-header-relay-federation

📦 Source in repository (JSON)

Answer

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'],
}

Evidence & signatures

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}
Generated from the verified corpus · MIT licensedBack to the catalog