OIDC-to-JWT bridge for Jitsi Meet. Authenticates meeting hosts through any OpenID Connect provider and issues Jitsi-compatible JWT tokens. Guests can join without authenticating -- they wait in the lobby until a host arrives.
This project was is heavily based on aadpM2hhdixoJm3u/jitsi-OIDC-adapter, who did the hard work of figuring out how to wedge OIDC into Jitsi's JWT auth flow. That project runs as a bare-metal Python service configured via INI files.
We needed something that works as a Docker container alongside Jitsi's own Docker deployment, configured entirely through environment variables. After auditing the original code, I found enough issues (secrets logged at debug level, no HTTP timeouts, room-wildcard JWTs, missing claims, hardcoded fallbacks) that a clean rewrite made more sense than a patch.
What changed:
- Docker-native -- Dockerfile, gunicorn, env vars, no config files
- No secrets in logs -- debug logging never prints credentials
- HTTP timeouts -- all outbound requests have a 10s timeout
- Room-scoped JWTs -- tokens are locked to the room being joined
- Standard OIDC claims -- tries
name,preferred_username,displayNamein that order instead of onlydisplayName - Proper JWT claims -- includes
iat,nbf,sub, 3h expiry - Health check --
/oidc/healthfor container orchestration - No error leaks -- exception details stay in server logs
- User tries to join a Jitsi room
- Jitsi redirects to
/oidc/auth?room={room} - Adapter redirects to the OIDC provider (Authentik, Keycloak, Auth0, etc.)
- User authenticates with the provider
- Provider redirects back to
/oidc/redirectwith an auth code - Adapter exchanges the code for tokens, validates the ID token
- Adapter issues a Jitsi JWT scoped to the room
- User lands in the meeting as an authenticated host
All configuration is via environment variables:
| Variable | Required | Default | Description |
|---|---|---|---|
OIDC_CLIENT_ID |
yes | OAuth2 client ID | |
OIDC_CLIENT_SECRET |
yes | OAuth2 client secret | |
OIDC_DISCOVERY_URL |
yes | OIDC discovery endpoint (.well-known/openid-configuration) |
|
OIDC_SCOPE |
no | openid email profile |
Scopes to request |
JITSI_BASE_URL |
yes | Public URL of your Jitsi instance (e.g. https://meet.example.com) |
|
JWT_APP_ID |
no | jitsi |
Must match Jitsi's JWT_APP_ID |
JWT_APP_SECRET |
yes | Must match Jitsi's JWT_APP_SECRET |
|
JWT_SUBJECT |
no | meet.example.com |
JWT sub claim, typically your Jitsi domain |
LOG_LEVEL |
no | INFO |
Python log level |
The adapter runs as a sidecar alongside the standard Jitsi Docker containers. Here's the relevant snippet:
services:
jitsi-oidc-adapter:
build: ./jitsi-oidc-adapter
# or: image: ghcr.io/dciww/jitsi-oidc-adapter:latest
restart: unless-stopped
environment:
OIDC_CLIENT_ID: ${OIDC_CLIENT_ID}
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET}
OIDC_DISCOVERY_URL: ${OIDC_DISCOVERY_URL}
JITSI_BASE_URL: https://meet.example.com
JWT_APP_ID: ${JWT_APP_ID}
JWT_APP_SECRET: ${JWT_APP_SECRET}
JWT_SUBJECT: meet.example.com
labels:
- "traefik.enable=true"
- "traefik.http.routers.jitsi-oidc.rule=Host(`meet.example.com`) && PathPrefix(`/oidc`)"
- "traefik.http.services.jitsi-oidc.loadbalancer.server.port=8000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/oidc/health"]
interval: 30s
timeout: 5s
retries: 3Your Jitsi web container needs these env vars to enable JWT auth and point unauthenticated users at the adapter:
AUTH_TYPE=jwt
TOKEN_AUTH_URL=https://meet.example.com/oidc/auth?room={room}
ENABLE_GUESTS=1
pip install -r requirements.txt
export OIDC_CLIENT_ID=...
export OIDC_CLIENT_SECRET=...
export OIDC_DISCOVERY_URL=https://your-idp.com/.well-known/openid-configuration
export JITSI_BASE_URL=https://meet.example.com
export JWT_APP_SECRET=...
gunicorn --bind 0.0.0.0:8000 --workers 2 app:appThen point your reverse proxy's /oidc/* paths at port 8000.
The body.html file contains JavaScript that intercepts Jitsi's "I am the
host" login dialog and redirects to /oidc/auth instead. Mount or copy it
to your Jitsi web container's document root and configure Jitsi to serve it:
# In your Jitsi web container or Nginx config
set $body_html_location /path/to/body.html;
location = /body.html {
alias $body_html_location;
}
Apache License 2.0