{"id":3314,"date":"2026-06-10T05:16:29","date_gmt":"2026-06-10T05:16:29","guid":{"rendered":"https:\/\/muhammadsoliman.com\/?p=3314"},"modified":"2026-06-10T05:23:09","modified_gmt":"2026-06-10T05:23:09","slug":"how-i-connected-wordpress-to-self-hosted-jitsi-meet-using-jwt-tokens","status":"publish","type":"post","link":"https:\/\/muhammadsoliman.com\/index.php\/2026\/06\/10\/how-i-connected-wordpress-to-self-hosted-jitsi-meet-using-jwt-tokens\/","title":{"rendered":"How I Connected WordPress to Self Hosted Jitsi Meet Using JWT Tokens"},"content":{"rendered":"\n<h1 class=\"wp-block-heading\"> JWT Token with Jitsi<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">When I first started building my own video classroom system, my goal was simple:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I wanted teachers to log into our WordPress\/BuddyBoss portal, click <strong>Start Class<\/strong>, and enter their assigned Jitsi classroom automatically \u2014 without needing a second Jitsi username and password.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Students, on the other hand, needed to stay simple. They should be able to open the classroom link and join as guests after the teacher starts the room.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This article explains how I tested and successfully connected WordPress to a self-hosted Jitsi Meet server using JWT tokens.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Starting Point<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before JWT, my Jitsi production system was already working.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The production Jitsi server used:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A dedicated Jitsi VM<\/li>\n\n\n\n<li>Internal Jitsi authentication<\/li>\n\n\n\n<li>Teacher usernames and passwords<\/li>\n\n\n\n<li>Guest students<\/li>\n\n\n\n<li>Permanent classroom links<\/li>\n\n\n\n<li>Stable video\/audio<\/li>\n\n\n\n<li>TURN and UDP media support<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The teacher flow was:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Teacher opens their permanent Jitsi classroom link.<\/li>\n\n\n\n<li>Teacher enters their Jitsi username and password.<\/li>\n\n\n\n<li>Teacher starts the room.<\/li>\n\n\n\n<li>Students join as guests.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">This worked, but it still required teachers to manage a separate Jitsi login. Since the teachers already log into the WordPress portal, I wanted WordPress to handle the Jitsi entry.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why JWT?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">JWT stands for JSON Web Token.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In this setup, JWT allows WordPress to create a secure, short-lived \u201centry ticket\u201d for Jitsi.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of the teacher typing a Jitsi username and password, WordPress generates a token that says:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u201cThis logged-in teacher is allowed to enter this exact room as a moderator.\u201d<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The Jitsi URL looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:&#47;&#47;jwt-meet.example.com\/teacher-room-name?jwt=SECURE_TOKEN\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The token is created server-side by WordPress, signed with a secret that Jitsi also knows, and expires after a short period.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is important because the full token URL should not be permanent. It should be generated fresh each time the teacher clicks the button.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Safe Testing Strategy<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The most important decision was this:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I did not switch my production Jitsi server directly to JWT.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead, I created a separate JWT test environment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The final strategy was:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Production Jitsi VM:\nmeet.example.com\nInternal authentication\nStill used for live classes\n\nJWT Test Jitsi VM:\njwt-meet.example.com\nJWT authentication\nUsed only for testing\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This gave me a safe lab to test JWT without risking live classes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The test server was created from the working production Jitsi setup, then modified carefully:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>New hostname<\/li>\n\n\n\n<li>New private IP<\/li>\n\n\n\n<li>New test domain<\/li>\n\n\n\n<li>New Jitsi public URL<\/li>\n\n\n\n<li>JWT authentication enabled<\/li>\n\n\n\n<li>Separate UDP media port<\/li>\n\n\n\n<li>Separate Traefik route<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">This allowed production to stay untouched while the JWT workflow was developed.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Network and Firewall Lessons<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The web part of Jitsi worked quickly through Traefik.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The harder part was media.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Jitsi video\/audio does not rely only on HTTPS. The Jitsi Videobridge needs UDP media traffic.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In production, the media port was:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>UDP 10000\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For the JWT test VM, I used:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>UDP 11000\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">At first, the meeting opened but disconnected after a little while. The problem was not WordPress and not JWT. The issue was that UDP 11000 was forwarded in Proxmox NAT, but not yet allowed in the Hetzner firewall.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Once the firewall rule was expanded to allow the test media port, the meeting became stable.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That was an important reminder:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Jitsi web access and Jitsi media access are different things.\nTraefik handles HTTPS.\nJitsi media still needs proper UDP forwarding.\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">WordPress Integration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">For the WordPress side, I used a safe two-part method.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. Store the JWT Secret in wp-config.php<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The JWT secret should not be stored directly inside a snippet or displayed inside the WordPress admin interface.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead, I placed it in <code>wp-config.php<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>define('ULAMA_JITSI_JWT_TEST_DOMAIN', 'jwt-meet.example.com');\ndefine('ULAMA_JITSI_JWT_TEST_APP_ID', 'ulama-jwt-test');\ndefine('ULAMA_JITSI_JWT_TEST_APP_SECRET', 'PASTE_SECRET_HERE');\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This keeps the secret outside the visible WordPress snippet editor.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. Use Fluent Snippets for the Button Logic<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">I used Fluent Snippets to create a WordPress shortcode.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The snippet type was:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Functions \/ PHP Functions\nPriority: 10\nRun: Everywhere\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The shortcode generated a fresh JWT token and displayed a simple button:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;ulama_jitsi_soliman_jwt_test]\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The button showed:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>My Live Classroom\nClick below to open your assigned Ulama classroom.\nStart Class\nTeacher access link expires in 4 hours.\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">When the teacher clicked <strong>Start Class<\/strong>, WordPress generated a fresh JWT link and opened the Jitsi room.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">BuddyBoss \/ BuddyDev Tab Setup<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I used BuddyDev\/BuddyPress User Profile Tabs Creator Pro to create the teacher meeting tab.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One important lesson:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Do not put the JWT link directly into the BuddyDev tab URL.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Why?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Because JWT links expire.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead, I placed the shortcode inside the subnav content area:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;ulama_jitsi_soliman_jwt_test]\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That way, WordPress generates a fresh token each time the page loads.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The final method was:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>BuddyDev subnav content \u2192 shortcode \u2192 WordPress generates JWT \u2192 teacher clicks Start Class\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This avoided iframe issues and avoided static expired links.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Troubleshooting Problems I Hit<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Problem 1: \u201cAuthentication failed\u201d<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">At one point, the Jitsi page showed:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Authentication failed\nSorry, you're not allowed to join this call.\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The cause was simple but easy to miss.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The JWT secret in <code>wp-config.php<\/code> did not exactly match the secret on the Jitsi server.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Mistakes included:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Pasting the full command output instead of only the secret<\/li>\n\n\n\n<li>Including JSON text around the secret<\/li>\n\n\n\n<li>Accidentally including <code>\\n<\/code> before and after the secret<\/li>\n\n\n\n<li>Using an old cached token after rotating the secret<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The correct secret format must be only the secret value:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>define('ULAMA_JITSI_JWT_TEST_APP_SECRET', '64_CHARACTER_SECRET_ONLY');\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">No spaces.<br>No line breaks.<br>No JSON.<br>No labels.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Problem 2: Meeting Opened but Disconnected<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This was caused by the test Jitsi media port not being allowed in the firewall.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The fix was:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Allow UDP 11000 for the JWT test VM\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">After that, video and audio stayed stable.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Problem 3: Snippet HTML Broke<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">At one point, the PHP snippet broke because of mixed PHP\/HTML and a missing closing <code>});<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The fix was to make the snippet return the HTML as a string instead of mixing too much open\/close PHP.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Final Successful Result<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The final test succeeded.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The working flow is now:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Teacher logs into WordPress\/BuddyBoss.<\/li>\n\n\n\n<li>Teacher opens the meeting tab.<\/li>\n\n\n\n<li>Teacher clicks <strong>Start Class<\/strong>.<\/li>\n\n\n\n<li>WordPress creates a fresh JWT token.<\/li>\n\n\n\n<li>Teacher enters Jitsi automatically as moderator.<\/li>\n\n\n\n<li>Teacher does not type a Jitsi username or password.<\/li>\n\n\n\n<li>Student\/guest link works.<\/li>\n\n\n\n<li>Audio and video are stable.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">This proved that the WordPress-to-Jitsi JWT workflow works.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why Production Was Not Changed Yet<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Even though the test worked, I did not immediately switch production.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The production server is still working with internal authentication, and live classes depend on it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The safe production plan is:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Keep the JWT test VM as a reference.<\/li>\n\n\n\n<li>Prepare a production maintenance window.<\/li>\n\n\n\n<li>Take a full VM snapshot of production Jitsi.<\/li>\n\n\n\n<li>Back up the production <code>.env<\/code>.<\/li>\n\n\n\n<li>Generate a new production JWT secret.<\/li>\n\n\n\n<li>Add production JWT constants to WordPress.<\/li>\n\n\n\n<li>Switch production Jitsi from internal auth to JWT.<\/li>\n\n\n\n<li>Test one teacher room first.<\/li>\n\n\n\n<li>Test student guest access.<\/li>\n\n\n\n<li>Keep rollback ready.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">The final strategy is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Do not move the test VM into production.\nUse the test VM as the lab.\nCarefully rebuild the proven JWT method on the original production Jitsi VM.\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Final Thoughts<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This project proved something important:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A self-hosted Jitsi Meet server can be connected to a WordPress\/BuddyBoss teacher portal using JWT tokens.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The result is much cleaner for teachers.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of remembering another Jitsi password, the teacher simply logs into WordPress and clicks <strong>Start Class<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Students still have a simple guest experience.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The key lessons were:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Test JWT on a separate VM first.<\/li>\n\n\n\n<li>Do not risk production too early.<\/li>\n\n\n\n<li>Store JWT secrets safely.<\/li>\n\n\n\n<li>Generate tokens server-side only.<\/li>\n\n\n\n<li>Never use fixed JWT links.<\/li>\n\n\n\n<li>Remember that Jitsi media needs UDP, not just HTTPS.<\/li>\n\n\n\n<li>Keep snapshots and rollback plans ready.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">This was not a one-click setup, but it created a strong foundation for a real online classroom platform.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>JWT Token with Jitsi When I first started building my own video classroom system, my goal&#8230;<\/p>\n","protected":false},"author":1,"featured_media":3315,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_kad_blocks_custom_css":"","_kad_blocks_head_custom_js":"","_kad_blocks_body_custom_js":"","_kad_blocks_footer_custom_js":"","_kadence_starter_templates_imported_post":false,"_kad_post_transparent":"","_kad_post_title":"","_kad_post_layout":"","_kad_post_sidebar_id":"","_kad_post_content_style":"unboxed","_kad_post_vertical_padding":"","_kad_post_feature":"","_kad_post_feature_position":"above","_kad_post_header":false,"_kad_post_footer":false,"_kad_post_classname":"","footnotes":""},"categories":[30,32,17],"tags":[],"class_list":["post-3314","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-server","category-proxmox","category-wordpress"],"taxonomy_info":{"category":[{"value":30,"label":"Server"},{"value":32,"label":"proxmox"},{"value":17,"label":"wordpress"}]},"featured_image_src_large":["https:\/\/muhammadsoliman.com\/wp-content\/uploads\/2026\/06\/ChatGPT-Image-Jun-10-2026-12_05_20-AM-1024x576.png",1024,576,true],"author_info":{"display_name":"Muhammad Soliman","author_link":"https:\/\/muhammadsoliman.com\/author\/muhmmad-soliman\/"},"comment_info":0,"category_info":[{"term_id":30,"name":"Server","slug":"server","term_group":0,"term_taxonomy_id":30,"taxonomy":"category","description":"","parent":0,"count":4,"filter":"raw","cat_ID":30,"category_count":4,"category_description":"","cat_name":"Server","category_nicename":"server","category_parent":0},{"term_id":32,"name":"proxmox","slug":"proxmox","term_group":0,"term_taxonomy_id":32,"taxonomy":"category","description":"","parent":0,"count":3,"filter":"raw","cat_ID":32,"category_count":3,"category_description":"","cat_name":"proxmox","category_nicename":"proxmox","category_parent":0},{"term_id":17,"name":"wordpress","slug":"wordpress","term_group":0,"term_taxonomy_id":17,"taxonomy":"category","description":"","parent":0,"count":3,"filter":"raw","cat_ID":17,"category_count":3,"category_description":"","cat_name":"wordpress","category_nicename":"wordpress","category_parent":0}],"tag_info":false,"_links":{"self":[{"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/posts\/3314","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/comments?post=3314"}],"version-history":[{"count":2,"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/posts\/3314\/revisions"}],"predecessor-version":[{"id":3317,"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/posts\/3314\/revisions\/3317"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/media\/3315"}],"wp:attachment":[{"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/media?parent=3314"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/categories?post=3314"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/muhammadsoliman.com\/index.php\/wp-json\/wp\/v2\/tags?post=3314"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}