Jitsi-meet-tokens chronicles on Debian Buster

It’s not possible to set a password or activate the lobby room using JWT now.

Use the following fields to control screen-sharing, recording, streaming features:

$payload = array(
    "context" => array(
        "user" => array(
        "features" => array(
            "recording" => true,
            "livestreaming" => true,
            "screen-sharing" => true,

Enable enableFeaturesBasedOnToken in your /etc/jitsi/meet/YOUR-DOMAIN-config.js for the token based feature control to work

I found this thread, because I am planning to setup token-based authentication and I was looking for a solution where I get the tokens from. I understood how the tokens should look like in theory and I understood the PHP code. But I do not understand, how I make that PHP code interact with Jitsi/Prosody.

  1. I want to manage my user accounts self-hosted. Do I need to install a full identity provider like Keycloak or is there an more light-weight/easier solution given the fact that user accounts shall be self-hosted (no external identity provider)?

  2. I don’t understand the overall architecture. If I set authentication to internal_hashed, then Jitsi Meet is responsible to create HTML-based user/password dialog and somehow forwards the given credentials to Prosody which perfoms authentication internally. If I enable token-based authentication, does Jitsi still renders the user/password dialog and then interacts with the identity provider (variant 1) or does the client directly interact with the identity provider which is in charge to provide a password dialog (variant 2)? I try to make it clear in two pictures.

    Variant 1:

          Client                                                  Jitsi               Identity Provider
            |              <-- HTML password dialog ---             |                        |
            |                                                       |                        |
    User enters password                                            |                        |
            |                                                       |                        |
            |        --- HTML POST with Username/Password -->       |                        |
            |                                                       |                        |
            |                                             Create JWT request incl.           |
            |                                               credentials of user              |
            |                                                       |                        |
            |                                                       |  --- JWT request -->   |
            |                                                       |                        |
            |                                                       |               Authorize JWT request
            |                                                       |         Check user credentials and rights
            |                                                       |                Create JWT response
            |                                                       |                        |
            |                                                       |  <-- JWT response ---  |
            |                                                       |                        |
            |                                                  Validate JWT                  |
            |                                                       |                        |              
            |  <-- Let user in (new HTML page or HTTP redirect ---  |                        |

    Variant 2:

          Client                                                  Jitsi               Identity Provider
            |       <-- Redirect to Identity Provider ---           |                        |
            |                                                       |                        |
            |  --------------------------- Ask for Authentication ------------------------>  |
            |                                                       |                        |
            |  <------------------------- HTML password dialog ----------------------------  |
            |                                                       |                        |
            |  -------------- HTML POST with Username/Password --------------------------->  |
            |                                                       |                        |
            |                                                       |         Check user credentials and rights
            |                                                       |                Create JWT response
            |                                                       |                        |
            |  <--------------------------------- JWT response ----------------------------  |
            |                                                       |                        |
            |  ------------------- Forward JWT ------------------>  |                        |
            |                                                       |                        |
            |                                                  Validate JWT                  |
            |                                                       |                        |
            |  <-- Let user in (new HTML page or HTTP redirect ---  |                        |

    Which variant is correct?

Jitsi does not interact with the identity provider or even the client for authentication in JWT case. The client connects to jitsi server using a link with a token. Something like that:


None of them

Lightweight solution is OK. After the successful login, redirect the client to jitsi server with a tokenized link.

mod_token_verification (a Prosody plugin) parses the token which is already given in the link and if it’s valid (correctly parsed using the shared secret) allows to login

Thanks, I believe, I got it. So basically, I set up my own HTML front page with a login form (room name, username, password), post it to my own sever-side script which does the actual authentication and then redirect the client to a room with a tokenized URL. Right?

What do you mean by something like that? Is the query parameter jwt correct and do I only have to replace very-long-jwt-value by a correct JWT based on your PHP script above or is there more to the story?

I assume, I got it. See my summary above. Is there already some lightweight solution which I can use out-of-the box and which you can recommend?




JWT with PHP

mod_token_affiliation compatible token


apt-get install php-cli composer
composer require firebase/php-jwt

Simple code


require_once 'vendor/autoload.php';
use \Firebase\JWT\JWT;

$LINK = "https://meet.mydomain.com";
$ROOM = "myroom";

$key = "mysecret";
$payload = array(
    "aud" => "myapp",
    "iss" => "myapp",
    "sub" => "meet.mydomain.com",
    "exp" => time() + (60*60),
    "room" => "$ROOM",
    "context" => array(
        "user" => array(
            "name" => "username",
            "email" => "username@mydomain.com",
            "avatar" => "https://gravatar.com/avatar/abc123.png",
            "affiliation" => "owner",
        "features" => array(
            "recording" => true,
            "livestreaming" => true,
            "screen-sharing" => true,

$jwt = JWT::encode($payload, $key);
echo $LINK . '/' . $ROOM . '?jwt=' . $jwt;
echo "\n";


php jwt.php

I finally got token-based authentication working mostly. But I observe some strange effects. My PHP code to create a token looks as follows. The PHP variable $participation is the result of a SQL query. I hope you get the intention based on the variable names:

$jwtPayload = array(
  "aud" => appName,
  "iss" => appName,
  "sub" => domain,
  "nbf" => $participation['start_time'] - (5*60),
  "iat" => time(),
  "exp" => $participation['end_time'] + (60*60),
  "room" => $participation['conference_name'],
  "moderator" => $participation['is_moderator'],
  "context" => array(
    "user" => array(
      "name" => $participation['display_name'],
      "email" => $participation['email'],
      "affiliation" => $participation['is_moderator'] ? "owner" : "member",
    "features" => array(
      "recording" => $participation['may_record'],
      "livestreaming" => $participation['may_live_stream'],
      "screen-sharing" => $participation['may_share_screen'],

The effects I observe are as follows:

  1. A user which only has affiliation member cannot create a room. Such a user must wait until the room has been created by an owner and can join afterwards. → Good, result as expected
  2. However, if the last owner hast left the room, members may stay in the room as long as they want. They are not kicked out. → Bad, result is unexpected
  3. Granting rights to the features recording and livestreaming does not work as expected. The ability to record/livestream seems to be a conference wide setting depending on the values of the last joining user; i.e. if a user joins who must not record/livestream the feature get disabled for everybody, contrary if a user joins who is allowed to record/livestream the feature gets enabled for everybody → Bad, result is unexpected
  4. Everybody is allowed to share his/her screen irrespective of the boolean value of screen-sharing. → Bad, result is unexpected
  5. Everybody seems to have partial rights of a moderator, irrespective of the the boolean value of moderator. For example, everybody can mute everyone else. → Bad, result is unexpected
  6. Contrary, some rights of the moderator are missing for everyone, irrespective of the the boolean value of moderator. For example, the feature “kick out …” does not work at all. → Bad, result is unexpected
  7. The value of user.name is used as a display name for the video stripe at the right, the tile view and for status messages, i.e. Jon Doe has entered/left the room. → Good, result as expected

Update on item 2:

If a user who has been a participant leaves the room, the module Token Owner Party also broadcasts the message “The owner is gone”. Hence, it seems that a user affiliation somehow magically changes from “member” to “owner” at some point of time. The test user has definitely been a “member” in the first place, because the test user was not allowed to create the room.

I have none of these “bad results” in my system, probably there are some missing steps while installing. You can try jitsi-school-installer

I am very sure I did not miss any step. And I am not destroying my installation I got so far by running a script which simply floors my current setup. Instead, I would like to understand what is going on.

Do you have org.jitsi.jicofo.DISABLE_AUTO_OWNER=true in /etc/jitsi/jicofo/sip-communicator.properties

Yes, I do.


plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }
VirtualHost "jitsi.my-domain.tld"
    authentication = "token"
    app_id = "jitsi"
    app_secret = "<my_secret>"
    disable_room_name_constraints = true
    allow_empty_token = false
    modules_enabled = {
        "ping"; -- Enable mod_ping
    c2s_require_encryption = false
    lobby_muc = "lobby.jitsi.my-domain.tld"
    main_muc = "conference.jitsi.my-domain.tld"

Component "conference.jitsi.my-domain.tld" "muc"
    storage = "memory"
    modules_enabled = {
   admins = { "focus@auth.jitsi.my-domain.tld" }
   party_check_timeout = 60
   muc_room_locking = false
   muc_room_default_public_jids = true





var config = {
  hosts: {
    domain: 'jitsi.my-domain.tld',
    muc: 'conference.jitsi.my-domain.tld'
  bosh: '//jitsi.my-domain.tld/http-bind',
  clientNode: 'http://jitsi.org/jitsimeet',
  testing: {
    p2pTestMode: false
  enableNoAudioDetection: true,
  enableNoisyMicDetection: true,
  resolution: 720,
  constraints: {
    video: {
      height: {
        ideal: 720,
        max: 1080,
        min: 360
  disableSimulcast: true,
  enableLayerSuspension: true,
  desktopSharingFrameRate: {
    min: 5,
    max: 25
  channelLastN: -1,
  lastNLimits: {
    5:  10,
    10: 5,
    20: 2,
  requireDisplayName: true,
  enableWelcomePage: true,
  defaultLanguage: 'de',
  disableProfile: true,
  enableFeaturesBasedOnToken: true,
  disableThirdPartyRequests: true,
  p2p: {
    enabled: false,
    stunServers: [
      { urls: 'stun:meet-jit-si-turnrelay.jitsi.net:443' }
  analytics: {
  deploymentInfo: {
  doNotStoreRoom: true,

Is the attribute moderator inside the JWT really required and if yes, which component does it use where?

I create my token this way

$jwtPayload = array(
  "moderator" => $participation['is_moderator'],
  "context" => array(
    "user" => array(
      "affiliation" => $participation['is_moderator'] ? "owner" : "member",
    "features" => array(

I use that attribute, because @emrah used it in this post and other places. However, the example token of the school installer does not mention it.

token-moderation module uses the moderator field; token_affiliation module uses the affiliation field.

School installer doesn’t use token-moderation module, therefore no need the moderator field for it

Then let me ask: Do I need the token-moderation plugin?

I had a closer look on the source code of the modules token_affiliation and token_owner_party. First, let me stress that I do not know Lua nor have I ever scripted Prosody before. So it might be, that my observation a plainly wrong. However, I believe that the modules simply don’t do what they are supposed to do.

Basically, the module token_owner_party does two things:

  1. If a user is going to enter a room, the module checks if the token claims that the user is a moderator, owner or teacher. If the token does so, the user is let into to room. Otherwise, a user is only let into the room, if the room is already occupied by at least one other user.
  2. If a user leaves the room, the module checks if the user has been an ordinary participant. If so, nothing happens. (Because at least one moderator must still in the room.) If a moderator left the room, the modules checks if it does find another user who is a moderator. If not, a timer starts which kicks out all other users after the timer triggered. (I simplified the last part a little bit.)

However, what I do not see anywhere is any logic, assertions, checks or whatever which controls that only moderators are allowed to mute other participants, kick them out and alike.

@emrah Did you had a close look at my config files? Are there any obvious misconfigurations?

remove org.jitsi.jicofo.auth.URL=EXT_JWT:jitsi.my-domain.tld from /etc/jitsi/jicofo/sip-communicator.properties and restart the services.

The other things seem OK

And what is your prosody version?

There were actually two questions in my previous post, you only answered the second one :wink:

Also consider my explanation below that question.

Why? According to Secure Domain Setup it should be set, if token-based authentication is used. What exactly does this config option do?

# aptitude show prosody
Package: prosody                         
Version: 0.11.4-1
State: installed
Automatically installed: yes

A normal Ubuntu 20.04.1 LTS installation

After setting the log level to debug, I found the following entries in prosody.log

Jan 30 18:32:41 conference.jitsi.my-domain.tld:muc  debug   no occupant found for nocheintest@conference.jitsi.my-domain.tld/4ea0875a; creating new occupant object for 4ea0875a-c140-4c40-b2eb-53a7eba0e004@jitsi.my-domain.tld/H_oJwLSC
Jan 30 18:32:41 conference.jitsi.my-domain.tld:token_owner_party    debug   let the party begin
Jan 30 18:32:41 conference.jitsi.my-domain.tld:token_verification   debug   pre join: MUC room (nocheintest@conference.jitsi.my-domain.tld) <presence to='nocheintest@conference.jitsi.my-domain.tld/4ea0875a' from='4ea0875a-c140-4c40-b2eb-53a7eba0e004@jitsi.my-domain.tld/H_oJwLSC'><x xmlns='http://jabber.org/protocol/muc'/><stats-id>Ayla-0jM</stats-id><c hash='sha-1' ver='PlsZdq8nYYZ7tQJSmtZfoWftI5M=' xmlns='http://jabber.org/protocol/caps' node='http://jitsi.org/jitsimeet'/><email>John Doe@my-domain.tld</email><nick xmlns='http://jabber.org/protocol/nick'>Jon Doe</nick><audiomuted xmlns='http://jitsi.org/jitmeet/audio'>false</audiomuted><videoType xmlns='http://jitsi.org/jitmeet/video'>camera</videoType><videomuted xmlns='http://jitsi.org/jitmeet/video'>false</videomuted><x xmlns='vcard-temp:x:update'><photo/></x><identity><user><affiliation>owner</affiliation><name>Jon Doe</name><email>John Doe@my-domain.tld</email></user></identity></presence>
Jan 30 18:32:41 conference.jitsi.my-domain.tld:token_verification   debug   Session token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJqaXRzaSIsImlzcyI6ImppdHNpIiwic3ViIjoiaml0c2kubWhubmV0LmRlIiwibmJmIjoxNjExNTAyMTU1LCJpYXQiOjE2MTIwMjc5NTcsImV4cCI6MTYxMjExMDg1NSwicm9vbSI6Ik5vY2hFaW5UZXN0IiwibW9kZXJhdG9yIjp0cnVlLCJjb250ZXh0Ijp7InVzZXIiOnsibmFtZSI6Ik1hdHRoaWFzIE5hZ2VsIiwiZW1haWwiOiJtYXR0aGlhcy5oLm5hZ2VsQGhlcm1hbm4tZWhsZXJzLWtvbGxlZy5kZSIsImFmZmlsaWF0aW9uIjoib3duZXIifSwiZmVhdHVyZXMiOnsicmVjb3JkaW5nIjp0cnVlLCJsaXZlc3RyZWFtaW5nIjp0cnVlLCJzY3JlZW4tc2hhcmluZyI6dHJ1ZX19fQ.ku8KrRABUs1jjDqQBM5Um3C4M9rhl0yH_o0WRJzs2JA, session room: NochEinTest
Jan 30 18:32:41 conference.jitsi.my-domain.tld:token_verification   debug   Will verify token for user: 4ea0875a-c140-4c40-b2eb-53a7eba0e004@jitsi.my-domain.tld/H_oJwLSC, room: nocheintest@conference.jitsi.my-domain.tld/4ea0875a 
Jan 30 18:32:41 conference.jitsi.my-domain.tld:token_verification   debug   allowed: 4ea0875a-c140-4c40-b2eb-53a7eba0e004@jitsi.my-domain.tld/H_oJwLSC to enter/create room: nocheintest@conference.jitsi.my-domain.tld/4ea0875a


Jan 30 18:36:47 conference.jitsi.my-domain.tld:token_verification   debug   pre join: MUC room (nocheintest@conference.jitsi.my-domain.tld) <presence to='nocheintest@conference.jitsi.my-domain.tld/30c4e895' from='30c4e895-23d0-4070-8584-f6f3e433b61c@jitsi.my-domain.tld/hejlyO4K'><x xmlns='http://jabber.org/protocol/muc'/><stats-id>Malinda-xyJ</stats-id><c hash='sha-1' ver='PlsZdq8nYYZ7tQJSmtZfoWftI5M=' xmlns='http://jabber.org/protocol/caps' node='http://jitsi.org/jitsimeet'/><nick xmlns='http://jabber.org/protocol/nick'>Jane Doe</nick><audiomuted xmlns='http://jitsi.org/jitmeet/audio'>false</audiomuted><videoType xmlns='http://jitsi.org/jitmeet/video'>camera</videoType><videomuted xmlns='http://jitsi.org/jitmeet/video'>false</videomuted><x xmlns='vcard-temp:x:update'><photo/></x><identity><user><affiliation>member</affiliation><name>Jane Doe</name><email>userdata: (nil)</email></user></identity></presence>
Jan 30 18:36:47 conference.jitsi.my-domain.tld:token_verification   debug   Session token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJqaXRzaSIsImlzcyI6ImppdHNpIiwic3ViIjoiaml0c2kubWhubmV0LmRlIiwibmJmIjoxNjExNTAyMTU1LCJpYXQiOjE2MTIwMjgyMDMsImV4cCI6MTYxMjExMDg1NSwicm9vbSI6Ik5vY2hFaW5UZXN0IiwibW9kZXJhdG9yIjpmYWxzZSwiY29udGV4dCI6eyJ1c2VyIjp7Im5hbWUiOiJGYWJpZW5uZSBGaXNjaGVyIiwiZW1haWwiOm51bGwsImFmZmlsaWF0aW9uIjoibWVtYmVyIn0sImZlYXR1cmVzIjp7InJlY29yZGluZyI6ZmFsc2UsImxpdmVzdHJlYW1pbmciOmZhbHNlLCJzY3JlZW4tc2hhcmluZyI6ZmFsc2V9fX0.jWowp6ntovr097UFGWAkC_kqrHB-dgTqHXQYN6zTFsc, session room: NochEinTest
Jan 30 18:36:47 conference.jitsi.my-domain.tld:token_verification   debug   Will verify token for user: 30c4e895-23d0-4070-8584-f6f3e433b61c@jitsi.my-domain.tld/hejlyO4K, room: nocheintest@conference.jitsi.my-domain.tld/30c4e895 
Jan 30 18:36:47 conference.jitsi.my-domain.tld:token_verification   debug   allowed: 30c4e895-23d0-4070-8584-f6f3e433b61c@jitsi.my-domain.tld/hejlyO4K to enter/create room: nocheintest@conference.jitsi.my-domain.tld/30c4e895


Jan 30 18:40:21 conference.jitsi.my-domain.tld:token_owner_party    debug   an owner leaved, 4ea0875a-c140-4c40-b2eb-53a7eba0e004@jitsi.my-domain.tld/H_oJwLSC
Jan 30 18:40:21 conference.jitsi.my-domain.tld:token_owner_party    debug   an owner is here, 30c4e895-23d0-4070-8584-f6f3e433b61c@jitsi.my-domain.tld/hejlyO4K
  1. John Doe is authenticated successfully and is a owner (look out for <user>affiliation>owner</affiliation>). Jon Doe’s gets the internal ID H_oJwLSC.
  2. Jane Doe is authenticated successfully and is a member (look out for <user><affiliation>member</affiliation>. Jane Doe gets the internal ID hejlyO4K.
  3. John Doe leaves the conference (look out for an owner leaved ... H_oJwLSC)
  4. Jane Doe is considered to be the new owner (look out for an owner is here ... hejlyO4K)

The one million dollar question: Why is Jane Doe considered to be a moderator although authentication seem to be as expected?

Could you paste the output

ls -alh /usr/share/jitsi-meet/prosody-plugins/

First of all: Problem solved

If the module token_affiliation is used, the option org.jitsi.jicofo.auth.URL MUST NOT be set in etc/jitsi/jicofo/sip-communicator.properties.

After I had removed that option and had restarted the service, the problem vanished. Also, all other problems from my previous post disappeared, too. I also believe, that this is the same reason why the module token_affiliation does not work in a dockerized environment as described in this post and follow-ups (identical problem: all members unintendedly become owners, too).

As @slauth writes here, it might be helpful to shed some light on this issue:

  1. What is the URL org.jitsi.jicofo.auth.URL intended to do?
  2. How does it interact with the affiliation process of Prosody?
  3. Why does the guide on Secure Domain Setup says that it should be set for token-based authentication, if it is actually harmful?

Although, I am happy now that it basically works, I have some follow-up questions @emrah :wink:

  1. If a member is re-directed to the conference room with a JWT-url (i.e. https://jitsi.my-domain.tld/SomeRoomName?jwt=an-encoded-jwt-here) and the room does not exist yet, the user is redirected to a static error page which tells the user that the user is not authorized (/static/authError.html)

  2. If a user is kicked out by a moderator, then the user is disconnected from the conference and the screen simply turns gray, but the user’s browser remains on the page of the conference (e.g. https://jitsi.my-domain.tld/SomeRoomName).

  3. If a user tries to enter a room by directly typing the URL, but without the JWT-part (e.g. https://jitsi.my-domain.tld/SomeRoomName), then Jitsi Meet asks for a username and password. Of course, password-based authentication is unsuccessful and the dialog re-appears endlessly. The user is caught in an infinite trap.

Trivial question: Is it possible, to redirect the user back to the authentication page which generates the JWT in all of the three cases, such that the user can try to re-login?

1 Like

I have only a solution for the first one now


    <!--#include virtual="/base.html" -->
    <link rel="stylesheet" href="css/all.css"/>
    <!--#include virtual="/title.html" -->
    <meta http-equiv="refresh" content="5; URL=https://your.authentication.page/" />
    <div class="redirectPageMessage">Sorry! You are not allowed to be here :(</div>