From 9ea3a9e1731f327ffdbac207005cfbd53e42d516 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Aug 2026 11:09:31 +0000 Subject: [PATCH 1/7] docs: add Phase 0 archaeology and decisions for jitsi-admin extension Research H2-invent/jitsi-admin's API surface, OIDC auth model, iframe embeddability, and self-hosting requirements, and cross-reference the web-app-draw-io / web-app-external-sites iframe patterns already in this repo. Records open questions (D1-D4) instead of silently resolving them, per the archaeology brief; recommends holding Phase 1 implementation on a D1 sign-off (nested-iframe framing and WebRTC permission propagation for the live call are not verifiable from source alone). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01JWGj1sNpSCpr4P55UDUw44 Signed-off-by: Claude --- ARCHAEOLOGY.md | 320 +++++++++++++++++++++++++++++++++++++++++++++++++ DECISIONS.md | 140 ++++++++++++++++++++++ 2 files changed, 460 insertions(+) create mode 100644 ARCHAEOLOGY.md create mode 100644 DECISIONS.md diff --git a/ARCHAEOLOGY.md b/ARCHAEOLOGY.md new file mode 100644 index 000000000..09b9eb1cf --- /dev/null +++ b/ARCHAEOLOGY.md @@ -0,0 +1,320 @@ +# ARCHAEOLOGY.md — jitsi-admin oCIS Web Extension (Phase 0) + +Every claim below is cited. Where a claim could not be verified from public sources, it is marked +**not confirmed** rather than assumed. This document only covers Phase 0 (archaeology); no Phase 1 +code has been written. See `DECISIONS.md` for the D1–D4 sign-off status this archaeology produced. + +Research method note: this session's GitHub tooling is scoped to `owncloud/web-extensions` only, so +all `H2-invent/jitsi-admin` findings come from public `WebFetch`/`WebSearch` against GitHub's web UI, +raw file URLs, and the repo's GitHub Wiki — not the GitHub API/clone. Citations are the exact URLs +fetched. + +--- + +## 1. jitsi-admin surface + +### 1.1 REST/API surface + +**There is no `CHANGELOG.md`** in the repo (404 confirmed at +`raw.githubusercontent.com/H2-invent/jitsi-admin/master/CHANGELOG.md`). The changelog function is +served by `RELEASE_NOTE.md`. The entry the prompt asked about is real: version 1.3 contains +**"Add Api to change Server of Room to use auto provisioner"**, alongside "add @ servername to jwt +roomname claim" and "Add user preferences (dark/light mode, language, timezone) to JWT." +(`raw.githubusercontent.com/H2-invent/jitsi-admin/master/RELEASE_NOTE.md`) + +A real, documented API exists under `/api/v1/*`, implemented in `src/Controller/api/`: +`APIRoomController.php`, `APIUserController.php`, `ApiMoveRoomToOtherServerController.php`, +`ServerAPIController.php`, `APILicenseController.php`, `ApiThemeController.php`, +`ApiTranscriptionController.php`, `CallerController.php`, `CalloutAPIController.php`, +`ConferenceMapperController.php`, `EventSyncApiController.php` +(`github.com/H2-invent/jitsi-admin/tree/master/src/Controller/api`). Confirmed capabilities: + +| Capability | Finding | +|---|---| +| (a) Create room programmatically | **Yes.** `POST /api/v1/room` (`APIRoomController.php`), payload `email`, `keycloakId`, `server`, `start`, `duration`, `name`. Also `PUT`/`DELETE /api/v1/room`, `POST /api/v1/room/move` (`ApiMoveRoomToOtherServerController.php`). | +| (b) Joinable meeting URL | **Indirectly.** No endpoint returns a raw Jitsi/LiveKit URL string; `src/Service/JoinUrlGeneratorService.php` builds a link back into jitsi-admin's own `join_index`/`join_index_uid` routes — jitsi-admin always mediates the join, it doesn't hand out a bare conference URL. | +| (c) JWT scoped to room + participant | **Yes**, the most solid part of the API. `src/Service/RoomService.php::genereateJwtPayload()` builds a JWT (`aud`=jitsi_admin, `iss`=AppId, `sub`=server URL, `room`, `context.user.name`, moderator flag, avatar; for LiveKit, `context.user.identity = "meetling_" + slug(userName) + "_" + randomSuffix`). Signed HS256 with the registered `Server`'s app secret (`JWT::encode(..., $room->getServer()->getAppSecret(), 'HS256')`); LiveKit goes through a separate encrypted-secret path. No `exp` claim was visible in the fetched excerpt. | +| (d) Invite by email/username | **Yes.** `POST`/`DELETE /api/v1/user` (`APIUserController.php`). `src/Service/RoomAddService.php::createUserFromUserUid()` resolves email-or-username, optionally auto-creates a user from an email when `strict_allow_user_creation` is set, and sends invite mail via Twig templates (e.g. `email/repeaterNew.html.twig`). | + +**API auth is a static per-server credential, not a user token.** `src/Helper/BearerTokenAuthHelper.php` +only regex-parses `Bearer ` — it does no validation. Every controller above then checks that +token string against the **`apiKey` field of a `Server` entity** in the database (the Jitsi/LiveKit +server record the admin registered), not a user identity. The Wiki's own `API-Endpoints` page states +outright (German, `raw.githubusercontent.com/wiki/H2-invent/jitsi-admin/API-Endpoints.md`): *"Es +sollten nur Zugriffe von einem Backend-Server auf den Jitsi-Admin durchgeführt werden"* — "only +backend-server-to-backend-server access should be performed against jitsi-admin." `config/packages/security.yaml` +corroborates: `/api/` sits under `PUBLIC_ACCESS` in `access_control` — there is no Symfony firewall +guarding it, auth is entirely ad hoc inside each controller. + +**This directly answers half of D3**: the API is designed for server-to-server calls with a +pre-shared key, never for a browser calling it directly as an authenticated end user. + +### 1.2 Auth model + +`composer.json` (`raw.githubusercontent.com/H2-invent/jitsi-admin/master/composer.json`) confirms +OIDC client packages: `stevenmaguire/oauth2-keycloak`, `knpuniversity/oauth2-client-bundle` +(config: `config/packages/knpu_oauth2_client.yaml`), plus `symfony/ldap` (LDAP is also supported, +separately) and `symfony/security-bundle`. + +- `LICENSE` note: `composer.json`'s `license` field reads `"proprietary"`, which conflicts with the + brief's premise of AGPLv3. **Not confirmed** — this needs a direct read of the repo's root + `LICENSE` file before any legal reliance on AGPLv3; a WebFetch content-safety guardrail in the + research tooling prevented a clean raw-text fetch of that file in this pass (see note at the top + of this document — a tooling limitation, not a repo property). + +**OIDC end-user login — confirmed, Keycloak-based:** +- `src/Controller/LoginControllerKeycloak.php` exposes `/login`, `/register`, + `/login/keycloak_edit`, `/login/keycloak_password`; `/login` starts an Authorization Code flow + with scopes `['email','openid','profile']`, with optional `kc_idp_hint` for multi-tenant IdP + federation. +- `src/Security/KeycloakAuthenticator.php` handles the callback route `connect_keycloak_check` — + a classic three-legged OAuth2 authenticator that stores `id_token` in the **PHP session** + (`$request->getSession()->set('id_token', ...)`), i.e. cookie-based, not a stateless bearer flow. +- `config/packages/security.yaml`: a single "main" firewall using + `custom_authenticators: App\Security\KeycloakAuthenticator`, Doctrine user provider keyed on + `keycloakId`. No separate stateless API firewall exists. +- Wiki page `Organize-Jitsi-Servers-via-keycloak-groups` documents Keycloak-group-based access + control as a first-class, supported pattern, and the shipped `docker-compose.yml` includes a + `keycloak-ja` service by default (§1.4) — Keycloak/OIDC is core to the project, not bolted on. + +**Does the API accept a forwarded end-user OIDC bearer token? No — not found, and the evidence +points the other way.** Every `/api/v1/*` controller validates against the static `Server.apiKey` +(§1.1); `KeycloakAuthenticator.php` only fires on the interactive `connect_keycloak_check` route and +never inspects the `Authorization` header on API requests. No stateless OIDC resource-server guard +(e.g. a JWKS-validating bundle wired to `/api/`) was found anywhere in `config/packages/` or +`src/Security/`. + +**Conclusion, feeding directly into D2/D3:** end-user OIDC identity (session-based, Keycloak login) +and the API's Bearer-token scheme (a pre-provisioned per-Jitsi/LiveKit-server key) are two separate, +non-interoperating mechanisms in this codebase as far as public source shows. A user's own oCIS/ +Keycloak access token cannot be handed to jitsi-admin's `/api/v1/*` and expected to authenticate as +that user — the payload identifies the user by `email`/`keycloakId` fields, but the *caller* must +already hold the Server API key. + +### 1.3 Iframe embeddability / framing headers + +**(a) jitsi-admin's own dashboard/scheduling pages — no built-in anti-framing.** No +`X-Frame-Options` or CSP `frame-ancestors` was found anywhere in the app: not in +`traefik/traefik.toml` (confirmed: only `logLevel` + access-log config, no headers middleware — +`raw.githubusercontent.com/H2-invent/jitsi-admin/master/traefik/traefik.toml`); not in +`config/packages/` (only `nelmio_cors.yaml` exists — no `nelmio_security.yaml` or CSP bundle, +per the directory listing at `github.com/H2-invent/jitsi-admin/tree/master/config/packages`); not +in `src/EventListener/` (`CorsHeaderListener.php` sets only `Access-Control-Allow-Origin: *`; +`github.com/H2-invent/jitsi-admin/tree/master/src/EventListener`). `nelmio_cors.yaml` allows +GET/OPTIONS/POST/PUT/PATCH/DELETE with `Content-Type`/`Authorization` headers, origin from a +`CORS_ALLOW_ORIGIN` env var — **CORS is not a framing control**, and there is no framing hardening +to rely on. The official install guide (`installDocker.md`) states outright: *"The installation is +not production ready. So you have to apply your own security rules."* **Practical read**: jitsi-admin +itself won't refuse to be framed out of the box, but nothing guarantees an operator's reverse proxy +won't add restrictive headers either — this is left entirely to the deployer, undocumented. + +**(b) The actual Jitsi Meet / LiveKit conference room — a different story, and the harder problem.** +There is a dedicated wiki page for exactly this: +**"Add jitsi admin to an allowed frame ancestor"** +(`github.com/H2-invent/jitsi-admin/wiki/Add-jitsi-admin-to-an-allowed-frame-ancestor`). It targets +hardening the **separate Jitsi Meet host** (e.g. `meet.domain.org`), and its recommended Nginx +config sets a CSP with `frame-ancestors` explicitly limited to the jitsi-admin domain, the meet +domain itself, and `file://`. This confirms a stock Jitsi Meet install may **not** permit arbitrary +third-party framing by default, and H2-invent's own guidance is to widen `frame-ancestors` to name +specific allowed domains — never to allow arbitrary origins. **To embed the live call itself inside +an oCIS web extension, the operator would additionally need to widen this `frame-ancestors` list to +include the oCIS origin on the Jitsi Meet server** — this is not supported out of the box. This wiki +page was **last edited March 2022**, predates jitsi-admin's LiveKit-first pivot, and says nothing +about LiveKit — **its currency for 2026 is not confirmed**. + +**How jitsi-admin embeds the conference itself:** `templates/start/index.html.twig` loads +`https://{{ room.server.url }}/external_api.js` and instantiates +`new JitsiMeetExternalAPI(...)` with `parentNode: document.querySelector('#jitsiWindow')` — i.e. +jitsi-admin uses the **official Jitsi Meet IFrame API**, not a bare `