diff --git a/CHANGELOG.md b/CHANGELOG.md index 55206086f..de365508a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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