🔒️(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-02 09:36:38 +02:00
committed by Manuel Raynaud
parent 6a5eb379c5
commit 175705ce80
2 changed files with 16 additions and 4 deletions
+6 -3
View File
@@ -223,12 +223,15 @@ and this project adheres to
so the Django backend can create documents without speaking yhub's lib0
wire encoding. Strict create (409 when the document already has content),
initial content attributed to the optional `X-User-Id` header; guarded by
standard document write access (admin JWT or user session)
standard document write access (the `aud: "yhub"` admin JWT, or a user
session with update ability)
- ✨(collaboration) add an admin reset-connections endpoint on yhub:
`POST /collaboration/reset-connections/v1/docs/{id}` re-checks the
authorization of the document's connected clients and disconnects (close
code 4401) only those whose access changed. Authenticated with the admin
JWT verified against the backend JWKS; not yet triggered by the backend on
code 4401) only those whose access changed. Authenticated with an admin JWT
verified against the backend JWKS and required to carry `aud: "yhub"`, so
an admin token Django issued for another service (e.g. the `y-converter`
one) cannot be replayed here; not yet triggered by the backend on
permission changes (follow-up)
- ⬆️(collaboration) upgrade yhub to 0.4.0 and serve all its routes under the
`/collaboration/` prefix (`server.apiPrefix`): the websocket moves to
+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