From a968dbe2b3cdc5c2c68666d2c3f04562ed3f567e Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Thu, 6 Aug 2026 12:12:05 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F(collaboration)=20reject?= =?UTF-8?q?=20admin=20jwts=20not=20issued=20for=20the=20yhub=20audience?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit yhub verified Django's RS256 admin JWT without checking "aud", so the y-converter token Django hands to the converter process was replayable here — and admin: true short-circuits getAccessType to "rw" on every document, plus the backend-internal reset-connections purpose and the X-User-Id attribution override. Require aud: "yhub", as y-provider already does for its own audience. Nothing in the backend calls yhub's admin endpoints yet, so no caller is affected. Signed-off-by: Kevin Jahns --- CHANGELOG.md | 1 + src/yhub-server/server.js | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3cc81b13..749f3a27a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to - 💥(y-provider) y-provider becomes converter-only - 💥(backend) move the resource server JWKS from `/api/{version}/jwks` to `/external_api/{version}/jwks` +- 🔒️(collaboration) reject admin jwts not issued for the yhub audience ### Fixed diff --git a/src/yhub-server/server.js b/src/yhub-server/server.js index 79d2cca02..ac782202d 100644 --- a/src/yhub-server/server.js +++ b/src/yhub-server/server.js @@ -22,6 +22,12 @@ const allowedOrigins = ( ).split(','); const Y_PROVIDER_API_KEY = secret('Y_PROVIDER_API_KEY', 'yprovider-api-key'); const ORG = process.env.YHUB_ORG || 'docs'; +// Requiring this audience stops a valid admin JWT that Django issued for +// another service (today: the y-converter token in converter_services.py, +// which is handed to the converter process) from being replayed against yhub. +// Hardcoded, like y-provider's Y_CONVERTER_AUDIENCE: both ends of a two-party +// contract, so an env var would only add a way to misconfigure it into a 401. +const YHUB_AUDIENCE = 'yhub'; // lowercase only (no /i): Django serializes UUIDs lowercase, while yhub rooms // and S3 keys are case-sensitive strings — accepting case variants would let a // client open a parallel room for the same document (and, with soft migration, @@ -407,6 +413,7 @@ const auth = createAuthPlugin({ // silently dropped as a 401. const { payload } = await jwtVerify(token, JWKS, { algorithms: ['RS256'], + audience: YHUB_AUDIENCE, clockTolerance: 5, }); // admin tokens act as the "system" user (no per-user admin identities yet) @@ -414,7 +421,9 @@ const auth = createAuthPlugin({ ? { userid: 'system', admin: true } : null; } catch { - return null; // bad signature / expired / JWKS unreachable — fail closed + // bad signature / expired / wrong (or missing) audience / JWKS + // unreachable — fail closed + return null; } } if (gcOff) return null; // full-history connections: not for Docs users