🔒️(collaboration) reject admin jwts not issued for the yhub audience

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 <kevin.jahns@protonmail.com>
This commit is contained in:
Kevin Jahns
2026-09-04 15:38:16 +02:00
committed by Anthony LC
parent 0714835a12
commit a968dbe2b3
2 changed files with 11 additions and 1 deletions
+1
View File
@@ -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
+10 -1
View File
@@ -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