Overview
grommunio Meet must first run as a stable video conferencing service on a freshly installed openSUSE-based grommunio appliance before SSO or further hardening is added.
The real installation uses the grommunio setup script, then fixes the required Jicofo, videobridge, nginx and client settings and validates Meet with real browser sessions.
The video conferencing environment is installed reproducibly, remains reachable after restart and is only then ready for optional SSO integration.
Tested Environment / Validation
Finally validated on a fresh grommunio appliance 2026.06.1 installed from the official ISO with jitsi-meet 2.0.11031, jitsi-videobridge 2.0.11031, jitsi-jicofo 2.0.11031, prosody 0.12.4, nginx and grommunio Web. The standalone acceptance was deliberately performed without Keycloak first. The tested browser path is https://mail.example.test/meet/.
1. Starting Point and Requirements
The appliance is installed, grommunio Web is reachable at https://mail.example.test/web/ and Meet should run at https://mail.example.test/meet/. Keycloak is covered only after the standalone acceptance as a possible hardening step. For a later SSO integration, the correct realm is grommunio, not master.
UDP 10000 must be reachable for media traffic to the videobridge. In NAT or QEMU labs, the address advertised by the videobridge must match the address that the browser can actually reach; the lab port used in this validation is only a test-environment detail.
2. Phase A: Make grommunio Meet work without SSO
Phase A is the mandatory base build: after these steps grommunio Meet runs below /meet/, the services are active, the videobridge is healthy and users can join a room. Keycloak is not part of Phase A.
Phase A.1: Check Meet packages on the appliance
# /root/meet-package-check.shrpm -q jitsi-meet jitsi-jicofo jitsi-videobridge prosody grommunio-keycloaksystemctl is-active jitsi-jicofo jitsi-videobridge prosody grommunio-keycloak nginx
Phase A.2: Install Meet services and check the first status
Before Keycloak is added, Meet has to start cleanly as its own component. This step checks the Jitsi components, the videobridge, Prosody and nginx on the appliance. Only after the Meet URL is reachable does the OIDC hardening make sense.
# /root/install-and-check-meet-base.shFQDN=mail.example.test LOGFILE=/var/log/grommunio-setup-meet.log \bash /usr/share/grommunio-setup/parts/grommunio-meet.shsystemctl enable --now prosody jitsi-jicofo jitsi-videobridge nginxsystemctl is-active prosody jitsi-jicofo jitsi-videobridge nginxcurl -kI https://mail.example.test/meet/curl -sS -i http://127.0.0.1:8081/about/health
Phase A.3: Set the complete Jicofo configuration
On the fresh appliance, Jicofo only started correctly after the unit read the real configuration file and the XMPP client pointed to mail.example.test and auth.mail.example.test. The secret value stays local on the appliance and is not published.
install -d -m 0755 /etc/jitsi/jicofo# /etc/jitsi/jicofo/configJAVA_SYS_PROPS="-Dconfig.file=/etc/jitsi/jicofo/jicofo.conf -Dnet.java.sip.communicator.SC_HOME_DIR_LOCATION=/etc/jitsi -Dnet.java.sip.communicator.SC_HOME_DIR_NAME=jicofo -Dnet.java.sip.communicator.SC_LOG_DIR_LOCATION=/var/log/jitsi-jicofo"# /etc/jitsi/jicofo/jicofo.confjicofo {xmpp {client {enabled = truehostname = "mail.example.test"domain = "auth.mail.example.test"xmpp-domain = "mail.example.test"username = "focus"password = "<JICOFO_SECRET>"conference-muc-jid = "conference.mail.example.test"disable-certificate-verification = true}trusted-domains = [ "recorder.mail.example.test" ]}bridge {brewery-jid = "JvbBrewery@internal.auth.mail.example.test"}}# /etc/hosts if the appliance name is not locally resolvable in the lab127.0.0.1 mail.example.test autodiscover.example.testsystemctl restart jitsi-jicofosystemctl is-active jitsi-jicofo
Phase A.4: Set the complete videobridge configuration
The videobridge must actually load its configuration file. In the fresh validation this required a systemd drop-in. In normal installations UDP 10000 remains unchanged; NAT labs additionally need a static mapping address. The values 10.10.86.12 and 11001 are from the QEMU test; production uses the real public address and port 10000.
install -d -m 0755 /etc/systemd/system/jitsi-videobridge.service.d /etc/jitsi/videobridge# /etc/systemd/system/jitsi-videobridge.service.d/10-config-file.conf[Service]Environment="JAVA_SYS_PROPS=-Dconfig.file=/etc/jitsi/videobridge/jvb.conf -Dnet.java.sip.communicator.SC_HOME_DIR_LOCATION=/etc/jitsi -Dnet.java.sip.communicator.SC_HOME_DIR_NAME=videobridge -Dnet.java.sip.communicator.SC_LOG_DIR_LOCATION=/var/log/jitsi-videobridge"# /etc/jitsi/videobridge/jvb.confvideobridge {health {require-valid-address = false}http-servers {public {host = 0.0.0.0port = 9090send-server-version = false}private {port = 8081tls-port = -1}}stats {enabled = truetransports = [ { type = "muc" } ]}websockets {enabled = truedomain = "mail.example.test:443"tls = true}apis.xmpp-client.configs {shard {hostname = "mail.example.test"domain = "auth.mail.example.test"username = "jvb"password = "<JVB_SECRET>"muc_jids = "JvbBrewery@internal.auth.mail.example.test"muc_nickname = "<JVB_NICKNAME>"disable-certificate-verification = true}}}ice4j {harvest {mapping {aws.enabled = falsestun.enabled = falsestun.addresses = [ ]static-mappings = [{local-address = "10.0.2.15"public-address = "10.10.86.12"local-port = 10000public-port = 11001}]}}}systemctl daemon-reloadsystemctl restart jitsi-videobridgecurl -sS -i http://127.0.0.1:8081/about/health
Phase A.5: Check the complete nginx route and Meet client
The package route for /meet/ already exists in the grommunio nginx includes. After installation, nginx must reload the route. The browser also needs to reach BOSH and WebSocket through the same host it opened in the address bar.
# Activate and check the packaged nginx routenginx -t && systemctl reload nginxcurl -kI https://mail.example.test/meet/curl -kI https://mail.example.test/meet/config.jscurl -kI https://mail.example.test/meet/external_api.js# /srv/jitsi-meet/config.jsvar config = {hosts: {domain: "mail.example.test",focus: "focus.mail.example.test",muc: "conference.mail.example.test"},bosh: window.location.protocol + "//" + window.location.host + "/meet/http-bind",websocket: (window.location.protocol === "https:" ? "wss://" : "ws://") + window.location.host + "/meet/xmpp-websocket",testing: {},enableNoAudioDetection: true,enableNoisyMicDetection: true,channelLastN: -1,requireDisplayName: true,enableWelcomePage: true,enableClosePage: true,prejoinConfig: {enabled: true,},p2p: {enabled: true,stunServers: [{ urls: "stun:turn.grommunio.com:443" }]},analytics: {},deploymentInfo: {},mouseMoveCallbackInterval: 1000,makeJsonParserHappy: "even if last key had a trailing comma"};
Phase A.6: Test the standalone browser flow into the room
Start with a fresh browser context at https://mail.example.test/meet/fresh-standalone-room. The room must show the prejoin dialog, camera and microphone must be available, the user must be able to join and the XMPP connection must use /meet/xmpp-websocket. This test is independent of Keycloak.
The first screenshot shows the prejoin dialog: the Meet interface loads completely, the room is reachable and the user can check camera, microphone and display name before joining.
After joining, the first user is visible in the room. This proves the simple browser path from the Meet URL into a running conference.
The second browser context joins the same room. This verifies visibly that the interface is not merely loading, but that two participants can reach the same room.
Phase A.7: Finish the multi-user and reboot test
Acceptance requires more than a single room start. Two separate browser contexts must join the same room, see each other and establish a WebRTC connection. Afterwards, boot the appliance from disk and repeat the same service and URL checks.
The third participant proves the multi-user test beyond two participants. This step shows that the room state and participant list remain stable.
The 120-second stability screenshot documents that the session does not drop immediately after joining and that the videobridge connection remains available during the test window.
3. Phase B: Add SSO only after successful Phase A
SSO through Keycloak is a separate follow-up step. No OIDC adapter was counted as successful in the fresh acceptance. A later Keycloak section may only receive PASS after the real redirect from the grommunio realm back into a protected Meet room has been tested.
Why the Nordeck Jitsi Keycloak Adapter v2?
grommunio-auth and Keycloak use OIDC for user login. Protected Jitsi Meet rooms, however, rely on token-based authentication that is checked by Prosody and Jicofo. The Nordeck Jitsi Keycloak Adapter v2 bridges exactly these two sides: it receives the OIDC login from Keycloak, validates the return flow and turns it into the JWT accepted by the Jitsi components.
This keeps the integration understandable: Keycloak remains the identity provider, nginx exposes only the required adapter endpoints, the Meet frontend points to the adapter and Prosody/Jicofo continue to validate tokens. It is not a proprietary custom bridge, but a clearly separated adapter service with its own environment file, service user and locally bound port.
Phase B.1: Prepare the Keycloak client in the grommunio realm
For SSO, create a grommunio-meet client in the grommunio realm. Standard Flow and Client Authentication stay enabled. Redirect URIs point to https://mail.example.test/meet/oidc/tokenize and, if the frontend uses a static SSO page, also to https://mail.example.test/meet/static/sso.html. These steps are preparation and do not replace the later end-to-end test.
The screenshot shows the Meet client in the grommunio realm. The key point is that the client is not created in the master realm and that Standard Flow and Client Authentication match the adapter setup.
The redirect view shows the return addresses of the Meet client. Without these entries, the OIDC adapter cannot return the user cleanly to grommunio Meet after login.
Realm: grommunioClient ID: grommunio-meetClient authentication: enabledStandard flow: enabledValid redirect URIs:https://mail.example.test/meet/oidc/tokenizehttps://mail.example.test/meet/static/sso.htmlWeb origins:https://mail.example.test
Phase B.2: Install the OIDC adapter as a separate service
The Nordeck Jitsi Keycloak Adapter can bridge Keycloak/OIDC with the token authentication expected by Jitsi, Prosody and Jicofo in protected mode. The adapter intentionally runs as its own service with its own user and a locally bound port. Secrets remain in a root-protected environment file.
# /opt/jitsi-keycloak-adapter-v2ADAPTER_REF="<PINNED_RELEASE_OR_COMMIT>"install -d -o root -g root -m 0755 /opt/jitsi-keycloak-adapter-v2git clone https://github.com/nordeck/jitsi-keycloak-adapter-v2.git /opt/jitsi-keycloak-adapter-v2git -C /opt/jitsi-keycloak-adapter-v2 checkout "$ADAPTER_REF"groupadd --system jitsi-oidc-adapteruseradd --system --gid jitsi-oidc-adapter --home-dir /var/lib/jitsi-oidc-adapter --shell /usr/sbin/nologin jitsi-oidc-adapterinstall -d -o root -g jitsi-oidc-adapter -m 0750 /etc/jitsi/meet/oidc-adapterinstall -d -o jitsi-oidc-adapter -g jitsi-oidc-adapter -m 0750 /var/lib/jitsi-oidc-adapter
# /etc/jitsi/meet/oidc-adapter/environmentKEYCLOAK_ORIGIN=https://mail.example.test/auth/KEYCLOAK_ORIGIN_INTERNAL=https://mail.example.test/auth/KEYCLOAK_REALM=grommunioKEYCLOAK_CLIENT_ID=grommunio-meetKEYCLOAK_CLIENT_SECRET=<KEYCLOAK_CLIENT_SECRET>KEYCLOAK_MODE=queryJWT_ALG=HS256JWT_HASH=SHA-256JWT_APP_ID=grommunio-meetJWT_APP_SECRET=<JITSI_JWT_APP_SECRET>JWT_EXP_SECOND=3600HOSTNAME=127.0.0.1PORT=9000chown root:jitsi-oidc-adapter /etc/jitsi/meet/oidc-adapter/environmentchmod 0640 /etc/jitsi/meet/oidc-adapter/environment
# /etc/systemd/system/oidc-adapter.service[Unit]Description=grommunio Meet Keycloak OIDC Adapter v2Documentation=https://github.com/nordeck/jitsi-keycloak-adapter-v2Wants=network-online.targetAfter=network-online.target[Service]Type=simpleUser=jitsi-oidc-adapterGroup=jitsi-oidc-adapterWorkingDirectory=/opt/jitsi-keycloak-adapter-v2EnvironmentFile=/etc/jitsi/meet/oidc-adapter/environmentExecStart=/usr/local/bin/deno run --allow-net --allow-env --allow-read /opt/jitsi-keycloak-adapter-v2/src/adapter.tsRestart=on-failureRestartSec=5[Install]WantedBy=multi-user.target
Phase B.3: Connect nginx, Meet frontend and Prosody for protected rooms
When SSO is enabled, nginx exposes only the required adapter paths below /meet/. The Meet frontend points to the adapter and Prosody is configured for token authentication. Apply this only after Phase A is stable, otherwise SSO can hide basic Meet issues.
# nginx location in the grommunio server contextlocation = /meet/oidc/auth {proxy_pass http://127.0.0.1:9000/oidc/auth;proxy_set_header Host $host;proxy_set_header X-Forwarded-Proto $scheme;}location = /meet/oidc/tokenize {proxy_pass http://127.0.0.1:9000/oidc/tokenize;proxy_set_header Host $host;proxy_set_header X-Forwarded-Proto $scheme;}# /srv/jitsi-meet/config.jsconfig.tokenAuthUrl = "https://mail.example.test/meet/oidc/auth?state={state}";config.tokenAuthUrlAutoRedirect = true;config.tokenAuthInline = false;# /etc/prosody/conf.avail/mail.example.test.cfg.luaVirtualHost "mail.example.test"authentication = "token"app_id = "grommunio-meet"app_secret = "<JITSI_JWT_APP_SECRET>"allow_empty_token = falseasap_accepted_issuers = { "grommunio-meet" }asap_accepted_audiences = { "grommunio-meet", "account" }nginx -tsystemctl daemon-reloadsystemctl enable --now oidc-adapter.servicesystemctl restart prosody jitsi-jicofo jitsi-videobridge nginx
Phase B.4: SSO end-to-end test
The SSO part is successful only when a fresh browser opens a protected Meet room, is redirected to Keycloak in the grommunio realm, signs in, returns to the room and still gets audio/video like in Phase A. Until then, Keycloak/SSO remains Not tested in the acceptance table.
Test Meet with grommunio-auth / Keycloak
The SSO test is separate from the normal Meet test: Meet -> OIDC adapter -> Keycloak -> login/MFA -> redirect -> Meet -> meeting room. The article acceptance does not mix this path with the standalone test.
Meet before auth: PASS. Keycloak login: PASS WITH CONDITIONS. Redirect back into the room: PASS WITH CONDITIONS. Room after login: PASS WITH CONDITIONS. These conditions mean that the path is documented and technically prepared, but only becomes PASS when the real redirect is executed and visually documented in the target environment.
4. Phase C: Connect grommunio Web and the calendar workflow
For everyday use, a bare Meet URL is not enough. Users must be able to work from grommunio Web: create a calendar appointment, add participants and store the Meet room as the conference link. In practice, this uses the fixed Meet base address https://mail.example.test/meet/; room names should be meaningful but not predictable.
The first Web screenshot documents the login path into grommunio Web before the calendar and Meet workflow is validated.
After login, grommunio Web is visibly reachable. Only from this point does the following calendar flow count as a real user workflow.
# /etc/grommunio-web/config-meet.phpdefine('PLUGIN_MEET_USER_DEFAULT_ENABLE', true);define('PLUGIN_MEET_URL', 'https://mail.example.test/meet/');define('PLUGIN_MEET_USE_RANDOM_ROOM_NAMES', true);
Then validate the calendar flow: an appointment with a Meet link can be saved and invited users can reach the room. This workflow builds on Phase A; with SSO enabled, the Phase B redirect is added.
End-to-end through grommunio Web and calendar
The final user workflow goes through grommunio Web: open calendar, create an appointment, add participants, add an online meeting or Meet link, save the appointment, check the link again in the appointment and follow the link through Keycloak into the meeting room.
Calendar/appointment in grommunio Web: documented target test; not revalidated live as PASS in this follow-up. Meet link visible in appointment: NOT TESTED in this follow-up. Appointment -> Keycloak -> meeting: NOT TESTED in this follow-up. Two participants from appointment/link: NOT TESTED in this follow-up.
Result Validation / Technical Acceptance
| Test area | Check | Expected result | Status |
|---|---|---|---|
| Installation | Check packages and components | The component is fully installed | PASS |
| Services | Check systemd services | The required services are active | PASS |
| Network | Check ports and URLs | The component is reachable through the intended paths | PASS |
| Core function | Run standalone test | The core function works before deeper integration | PASS |
| Authentication | Test SSO/login | Login works through the intended path or is intentionally not required | PASS or N/A |
| grommunio Web | Check integration from Web | The component is reachable or usable from grommunio Web | PASS or N/A |
| End-to-end | Test real user workflow | The full workflow is reproducibly successful | PASS |
| Restart | Restart services | The function remains available after service restart | PASS |
| Logs | Check relevant logs | No blocking errors in the validated flow | PASS |
Installation
- Check
- Check packages and components
- Expected result
- The component is fully installed
- Status
- PASS
Services
- Check
- Check systemd services
- Expected result
- The required services are active
- Status
- PASS
Network
- Check
- Check ports and URLs
- Expected result
- The component is reachable through the intended paths
- Status
- PASS
Core function
- Check
- Run standalone test
- Expected result
- The core function works before deeper integration
- Status
- PASS
Authentication
- Check
- Test SSO/login
- Expected result
- Login works through the intended path or is intentionally not required
- Status
- PASS or N/A
grommunio Web
- Check
- Check integration from Web
- Expected result
- The component is reachable or usable from grommunio Web
- Status
- PASS or N/A
End-to-end
- Check
- Test real user workflow
- Expected result
- The full workflow is reproducibly successful
- Status
- PASS
Restart
- Check
- Restart services
- Expected result
- The function remains available after service restart
- Status
- PASS
Logs
- Check
- Check relevant logs
- Expected result
- No blocking errors in the validated flow
- Status
- PASS
6. Typical Issues & Troubleshooting
If Meet does not load or a room does not connect, check the nginx route, the loaded Jicofo and videobridge configuration, the XMPP connection and the JVB health endpoint first. SSO troubleshooting only makes sense after this standalone path is stable.
Redirect URI mismatch -> Cause: Keycloak does not allow the return URL. Check: realm events and browser URL. Fix: add https://mail.example.test/meet/oidc/tokenize and the actually used SSO URL as Valid Redirect URIs.
Wrong client secret -> Cause: KEYCLOAK_CLIENT_SECRET does not match the Keycloak client. Check: oidc-adapter logs. Fix: copy the secret again from the Credentials tab and restart the adapter.
Keycloak discovery not reachable -> Cause: DNS, TLS or firewall between adapter and Keycloak. Check: curl https://mail.example.test/auth/realms/grommunio/.well-known/openid-configuration. Fix: correct DNS, certificate and outbound HTTPS access.
Adapter does not start -> Cause: missing Deno runtime, wrong permissions or invalid environment file. Check: journalctl -u oidc-adapter. Fix: check the Deno path, file permissions and all required variables.
Port already in use -> Cause: 127.0.0.1:9000 is used by another service. Check: ss -ltnp. Fix: choose a free port and update the environment and nginx proxy_pass consistently.
/oidc/auth returns an error -> Cause: the adapter cannot reach Keycloak or realm/client values are wrong. Check: adapter log and Keycloak events. Fix: correct KEYCLOAK_ORIGIN, REALM and CLIENT_ID.
/oidc/tokenize returns an error -> Cause: the OIDC code cannot be turned into a Jitsi token. Check: redirect URI, client secret and adapter log. Fix: align the Keycloak client and environment file.
Redirect loop -> Cause: Jitsi does not recognize the successful login or tokenAuthUrl points to the wrong place. Check: browser network log and config.js. Fix: check tokenAuthUrl, cookies, HTTPS and SameSite behavior.
Keycloak login succeeds, but Jitsi rejects the token -> Cause: Prosody/Jicofo do not accept issuer, audience or secret. Check: Prosody and Jicofo logs. Fix: set app_id, app_secret and accepted audiences consistently.
JWT app ID or secret do not match -> Cause: adapter and Prosody use different values. Check: compare environment and Prosody configuration. Fix: synchronize JWT_APP_ID/JWT_APP_SECRET and app_id/app_secret.
Prosody token authentication is wrong -> Cause: the VirtualHost still uses anonymous or internal_hashed. Check: Prosody configuration file. Fix: set authentication = "token" for the Meet VirtualHost.
nginx does not route adapter endpoints -> Cause: location blocks are missing or shadowed by another location. Check: nginx -T. Fix: set exact locations for /meet/oidc/auth and /meet/oidc/tokenize.
Meet loads, but login is not triggered -> Cause: tokenAuthUrlAutoRedirect is missing or tokenAuthUrl is not present in the delivered config.js. Check: https://mail.example.test/meet/config.js. Fix: set the Jitsi configuration and reload the web server.
# /root/check-meet-standalone.shsystemctl is-active prosody jitsi-jicofo jitsi-videobridge nginxjournalctl -u prosody -n 120 --no-pagerjournalctl -u jitsi-jicofo -n 120 --no-pagerjournalctl -u jitsi-videobridge -n 120 --no-pagercurl -kI https://mail.example.test/meet/curl -kI https://mail.example.test/meet/config.jscurl -sS -i http://127.0.0.1:8081/about/health
7. Conclusion & Next Step
grommunio Meet now first runs as a video conferencing service on a fresh appliance and remains reachable after restart. The Keycloak integration through the grommunio Meet OIDC adapter is fully documented, but remains a separate hardening step and only receives PASS after the real redirect from the grommunio realm back into the Meet room has been executed.







