JWT lets your application issue signed meeting access credentials. The backend must first decide whether the user may enter that meeting; signing a browser-supplied room or moderator request without that check creates an access-control problem. [S3][S6][S7]
Scope: source reviewed on 24 October 2026 against docker-jitsi-meet stable-11248-2, commit 74d3e6e, and Jitsi Meet 11248 source at f765042b. The Node.js and Python signing examples were exercised locally. This revision is not a full live Jitsi authentication or moderator-policy test and does not extend the older stable-11031 production baseline. [S1][S3]
1. Choose the access policy before enabling JWT
This example uses one self-hosted server, HS256 with a shared secret, literal room names and a token for every participant. It does not configure JaaS, multiple tenants, regular-expression room claims or an external public-key service. Those modes need their own issuer and verifier configuration. [S2][S3]
Back up your existing project and persistent configuration, then plan a maintenance window. In the Docker project’s .env, replace the secret placeholder and confirm the main XMPP domain: [S1][S2]
ENABLE_AUTH=1
AUTH_TYPE=jwt
JWT_APP_ID=my_app
JWT_APP_SECRET=REPLACE_WITH_A_PRIVATE_RANDOM_SECRET
JWT_ACCEPTED_ISSUERS=my_app
JWT_ACCEPTED_AUDIENCES=my_app
JWT_SIGN_TYPE=HS256
JWT_ENABLE_DOMAIN_VERIFICATION=1
JWT_ALLOW_EMPTY=0
ENABLE_GUESTS=0For a new dedicated JWT secret, generate 32 random bytes as 64 hexadecimal characters. Store the result privately in the Jitsi configuration and your backend’s secret store; do not commit it or show it in a screenshot. Both sides must use the same character string, without hex-decoding it on one side only. This is a JWT secret, not a reason to regenerate existing internal service passwords. [S1][S2][S6][S7][S11]
openssl rand -hex 32Apply the changed configuration with the same project, environment files, profiles and overlays used by the existing deployment. For a project normally started without extra options: [S10]
docker compose up -dThis may interrupt meetings. Compose recreates changed containers; restarting an existing container alone does not apply changed Compose environment variables. Preserve existing service passwords and storage. See upgrade preparation. [S1][S10]
2. Match claims to the verifier
The examples below issue a token for one authorized room and calculate its lifetime at signing time. Do not copy a fixed exp timestamp from a decoded sample. JWT NumericDate values use seconds, not JavaScript milliseconds. [S6][S7][S8]
| Field | Value in this guide | What to verify |
|---|---|---|
Header alg |
HS256 |
Matches the configured signature algorithm and shared secret |
iss |
my_app |
Accepted issuer list |
aud |
my_app |
Accepted audience list |
sub |
Main XMPP domain | With domain verification enabled, matches the target domain |
room |
One authorized literal room | Matches the actual meeting; no wildcard grant |
iat |
Current Unix seconds | Issuer clock is correct |
exp |
Current time plus 600 seconds | Future when verified; refresh only after another authorization check |
context.user.id |
Server-authenticated user ID | Derived from the trusted login session |
These values describe the explicit configuration above, not every supported Jitsi token format. The reviewed verifier requires sub and normally a room claim, validates the signed token and checks the room at MUC admission. Literal room claims are lowercased in the reviewed source; the examples choose a consistent lowercase naming convention. [S2][S3][S6][S7][S8]
Docker domain versus public hostname
Docker’s main XMPP_DOMAIN defaults to meet.jitsi, which may differ from the browser hostname meet.example.com. With JWT_ENABLE_DOMAIN_VERIFICATION=1, use that actual main XMPP domain as sub for this single-domain setup. The template’s default is verification off; this guide deliberately turns it on. Disabling the check to hide a mismatch changes the policy. [S2][S3]
Set these backend-only variables in your server environment or secret manager. JWT_SUB is an input to our examples, not a docker-jitsi-meet setting: [S2][S6][S7]
JWT_APP_ID=my_app
JWT_APP_SECRET=THE_SAME_PRIVATE_64_HEX_CHARACTER_SECRET
JWT_SUB=meet.jitsiReplace JWT_SUB if your main domain differs. Do not use * for the subject or room in an ordinary per-meeting issuer. The reviewed verifier supports wildcard access, which is broader than the policy these examples implement. [S3]
3. Authorize first, then sign on the backend
Both functions require lookupMeetingGrant(userId, meetingId), implemented by your application against trusted membership or booking data. It must return null/None for denied access, or an object with canJoin equal to the boolean true and the canonical room name. The user ID must come from a verified login session, not the request body. These are signing helpers, not ready-to-expose HTTP endpoints. [S3][S6][S7]
The room pattern is an application convention for these examples, not Jitsi’s universal naming limit. The helpers refuse wildcard rooms and do not copy arbitrary user-provided claims or moderator flags. Authorize role grants separately if your deployment uses them. [S3][S4]
Node.js
Install jsonwebtoken in your backend project. The example was checked with Node 22.23.4 and jsonwebtoken 9.0.3. Save as jitsi-token.mjs in an ES-module backend. [S6]
npm install jsonwebtokenimport jwt from "jsonwebtoken";
export async function issueJitsiToken(userId, meetingId, lookupMeetingGrant) {
if (typeof userId !== "string" || !userId ||
typeof meetingId !== "string" || !meetingId) {
throw new Error("Authentication and meeting ID required");
}
const grant = await lookupMeetingGrant(userId, meetingId);
if (grant?.canJoin !== true) throw new Error("Meeting access denied");
if (typeof grant.room !== "string" ||
!/^[a-z0-9][a-z0-9-]{0,63}$/.test(grant.room)) {
throw new Error("Invalid authorized room");
}
const appId = process.env.JWT_APP_ID;
const secret = process.env.JWT_APP_SECRET;
const subject = process.env.JWT_SUB;
if (!appId || !/^[0-9a-f]{64}$/.test(secret ?? "") ||
!subject || subject === "*") {
throw new Error("JWT server configuration missing or invalid");
}
return jwt.sign(
{ room: grant.room, context: { user: { id: userId } } },
secret,
{ algorithm: "HS256", issuer: appId, audience: appId,
subject, expiresIn: 600 },
);
}jsonwebtoken supplies iat and calculates exp from the numeric lifetime in seconds. Pass the actual authenticated user and an authorization lookup; do not replace the lookup with an unconditional allow response. [S6]
Python
Install PyJWT in your backend environment. This example was checked with PyJWT 2.15.1. Save as jitsi_token.py; the lookup in this version is synchronous. [S7]
python -m pip install PyJWTimport os
import re
import time
import jwt # PyJWT
def issue_jitsi_token(user_id, meeting_id, lookup_meeting_grant):
if not isinstance(user_id, str) or not user_id or \
not isinstance(meeting_id, str) or not meeting_id:
raise PermissionError("Authentication and meeting ID required")
grant = lookup_meeting_grant(user_id, meeting_id)
if not isinstance(grant, dict) or grant.get("canJoin") is not True:
raise PermissionError("Meeting access denied")
room = grant.get("room")
if not isinstance(room, str) or not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", room):
raise ValueError("Invalid authorized room")
app_id = os.environ.get("JWT_APP_ID")
secret = os.environ.get("JWT_APP_SECRET", "")
subject = os.environ.get("JWT_SUB")
if not app_id or not re.fullmatch(r"[0-9a-f]{64}", secret) or \
not subject or subject == "*":
raise ValueError("JWT server configuration missing or invalid")
now = int(time.time())
payload = {
"iss": app_id, "aud": app_id, "sub": subject,
"room": room, "iat": now, "exp": now + 600,
"context": {"user": {"id": user_id}},
}
return jwt.encode(payload, secret, algorithm="HS256")Keep credentials out of frontend bundles. Your HTTP handler still needs authenticated sessions, application authorization, appropriate request protection and rate limits. Return tokens over HTTPS with a policy that avoids caching and logging credentials. Treat issued tokens as sensitive bearer credentials; payloads are not encrypted merely because they are signed. [S6][S7][S8]
4. Pass only the finished token to the client
For an existing application using the IFrame API, supply the token received from your authenticated backend as the jwt option and use the same authorized room name as the token. The API also needs the server’s external_api.js; follow the complete embedding guide for loading, lifecycle and CSP details. [S9]
const api = new JitsiMeetExternalAPI("meet.example.com", {
roomName: authorizedRoomFromServer,
jwt: tokenFromAuthenticatedBackend,
parentNode: document.getElementById("meet"),
});A ?jwt=... meeting URL also carries a credential. Avoid sharing it publicly or placing it in analytics and support logs. Passing a token through an iframe option does not make it invisible to the person using the browser. Do not send the signing secret to the client. [S8][S9]
5. Verify guests and moderator permissions separately
Tokenless access
ENABLE_GUESTS=1 adds an anonymous guest route when authentication is enabled. JWT_ALLOW_EMPTY=1 instead allows an empty token on the token-authenticated host. They are different mechanisms, and neither should be enabled merely to make a failed JWT test pass. This guide leaves both disabled. [S2][S3]
If you intentionally allow guests, test tokenless entry before a host arrives, after the host arrives and with the lobby enabled. Host waiting and admission depend on the active modules and conference policy; the guest flag alone is not proof that every desired restriction is enforced. [S2][S5]
Role claims
The default Docker token-verification module is not proof that a particular token-affiliation policy is active. Inspect the installed module and the generated conference component’s modules_enabled. The pinned token_affiliation implementation maps context.user.moderator: true to owner; it also recognizes owner-like affiliation strings. A false moderator value does not override an owner affiliation. Other automatic owner policies can affect the result. [S2][S4][S5]
Our signing helpers omit role claims because they do not configure a role policy. If you add role claims, derive them from trusted backend permissions and follow the moderator diagnostic checklist. Verify both admission and moderator actions using separate host and participant sessions, including the participant arriving first and the host leaving and rejoining. Do not turn a reported module configuration into a universal fix. [S4][S5]
6. Test allowed and denied cases
Local signing-library tests are useful, but a library accepting a token is not proof that your Jitsi server accepts or scopes it correctly. Complete these deployment acceptance checks on a non-production setup: [S2][S3][S4][S5]
| Case | Intended result for this guide’s policy |
|---|---|
| Signed-in user lacks meeting permission | Backend refuses to issue a token |
| Valid authorized room token | User enters the matching meeting |
| Same token used for another room | Admission denied |
| Signature altered or wrong secret | Token rejected |
| Wrong issuer or audience | Token rejected |
| Wrong subject with domain verification enabled | Room admission denied |
| Expired token on a new connection | Token rejected |
| No token | Anonymous admission denied |
| Participant attempts moderator-only action | Denied under the chosen role policy |
Check server and issuer clocks when testing expiry. Do not assume a 10-minute token ends an already established call after 10 minutes; test rejoin and reconnect and implement meeting-duration policy separately. For a long meeting, a refresh endpoint must repeat authorization before issuing a new token. [S3][S6][S7][S8]
When diagnosing, inspect only the necessary sanitized fields and module names. Do not paste real tokens into online decoders or enable verbose production logging without reviewing what it exposes. See JWT failure diagnostics for existing failures. [S3][S8]
Debian and Ubuntu package differences
Docker .env flags do not configure package installations. Upstream documents jitsi-meet-tokens, token authentication in the main Prosody virtual host, and token_verification on the conference component. Package paths include /etc/prosody/conf.avail/meet.example.com.cfg.lua and /etc/jitsi/meet/meet.example.com-config.js; replace the example hostname. [S12]
Use the actual configured main XMPP domain for sub. The reviewed token utility defaults domain verification to true when no overriding setting is supplied; Docker’s template supplies its own default. Installed package versions and administrator overrides can differ, so inspect the active configuration. Do not transplant Docker Jicofo or guest settings into a package server: current upstream package guidance describes a different authentication arrangement. [S2][S3][S12]
Open questions and maintenance
We exercised the signing helpers with generated temporary secrets, tested denied access and invalid tokens, and checked Docker configuration propagation. No live meeting, Prosody room-admission, tokenless browser, role, reconnect or package-install test was completed for this revision. Both issuer implementations require your application’s real authorization lookup.
Recheck after token-library, Docker-template or role-module changes. Keep server-side access checks and secret rotation in the application’s maintenance plan. The next content review is the dedicated JWT troubleshooting page, including its diagnostic token examples.
Sources
Reviewed 24 October 2026; source snapshots describe code, not the contents of every installed image.
- [S1] Docker release stable-11248-2, Compose file and password generator.
- [S2] Pinned Docker Prosody template: JWT settings, domains, guests and modules.
- [S3] Jitsi Meet 11248 token utility, authentication module and room verifier.
- [S4] Pinned token affiliation module: owner and member mapping, with lobby-sensitive logic.
- [S5] Pinned Docker Jicofo template and upstream authentication design: verify conference and host policy separately.
- [S6] jsonwebtoken documentation: signing options, lifetime and verification. The application permission lookup is our integration boundary.
- [S7] PyJWT usage: encoding, claim verification and expiry. The application permission lookup is our integration boundary.
- [S8] RFC 7519: JWT claims, NumericDate and security considerations.
- [S9] Jitsi IFrame API: external API script, room and JWT options.
- [S10] Docker Compose up and restart: changed environments and recreation.
- [S11] OpenSSL rand: random bytes and hexadecimal output.
- [S12] Jitsi token authentication handbook: package configuration and differences from Docker.
Need help integrating an issuer with your application? See Jitsi integration support.