diff --git a/charts/sovereign-workplace-jitsi/files/prosody/jitsi-meet.cfg.lua b/charts/sovereign-workplace-jitsi/files/prosody/jitsi-meet.cfg.lua
new file mode 100644
index 0000000000000000000000000000000000000000..ba00798171078a2b8838d3c7aa5163323ee0011b
--- /dev/null
+++ b/charts/sovereign-workplace-jitsi/files/prosody/jitsi-meet.cfg.lua
@@ -0,0 +1,408 @@
+-- SPDX-FileCopyrightText: 2023 Bundesministerium des Innern und für Heimat, PG ZenDiS "Projektgruppe für Aufbau ZenDiS"
+-- SPDX-License-Identifier: Apache-2.0
+{{ $ENABLE_AUTH := .Env.ENABLE_AUTH | default "0" | toBool -}}
+{{ $AUTH_TYPE := .Env.AUTH_TYPE | default "internal" -}}
+{{ $PROSODY_AUTH_TYPE := .Env.PROSODY_AUTH_TYPE | default $AUTH_TYPE -}}
+{{ $ENABLE_GUEST_DOMAIN := and $ENABLE_AUTH (.Env.ENABLE_GUESTS | default "0" | toBool) -}}
+{{ $ENABLE_RECORDING := .Env.ENABLE_RECORDING | default "0" | toBool -}}
+{{ $JIBRI_XMPP_USER := .Env.JIBRI_XMPP_USER | default "jibri" -}}
+{{ $JIGASI_XMPP_USER := .Env.JIGASI_XMPP_USER | default "jigasi" -}}
+{{ $JVB_AUTH_USER := .Env.JVB_AUTH_USER | default "jvb" -}}
+{{ $JWT_ASAP_KEYSERVER := .Env.JWT_ASAP_KEYSERVER | default "" -}}
+{{ $JWT_ALLOW_EMPTY := .Env.JWT_ALLOW_EMPTY | default "0" | toBool -}}
+{{ $JWT_AUTH_TYPE := .Env.JWT_AUTH_TYPE | default "token" -}}
+{{ $JWT_ENABLE_DOMAIN_VERIFICATION := .Env.JWT_ENABLE_DOMAIN_VERIFICATION | default "false" | toBool -}}
+{{ $MATRIX_UVS_ISSUER := .Env.MATRIX_UVS_ISSUER | default "issuer" -}}
+{{ $MATRIX_UVS_SYNC_POWER_LEVELS := .Env.MATRIX_UVS_SYNC_POWER_LEVELS | default "0" | toBool -}}
+{{ $JWT_TOKEN_AUTH_MODULE := .Env.JWT_TOKEN_AUTH_MODULE | default "token_verification" -}}
+{{ $ENABLE_LOBBY := .Env.ENABLE_LOBBY | default "true" | toBool -}}
+{{ $ENABLE_AV_MODERATION := .Env.ENABLE_AV_MODERATION | default "true" | toBool -}}
+{{ $ENABLE_BREAKOUT_ROOMS := .Env.ENABLE_BREAKOUT_ROOMS | default "true" | toBool -}}
+{{ $ENABLE_END_CONFERENCE := .Env.ENABLE_END_CONFERENCE | default "true" | toBool -}}
+{{ $ENABLE_XMPP_WEBSOCKET := .Env.ENABLE_XMPP_WEBSOCKET | default "1" | toBool -}}
+{{ $ENABLE_JAAS_COMPONENTS := .Env.ENABLE_JAAS_COMPONENTS | default "0" | toBool -}}
+{{ $ENABLE_RATE_LIMITS := .Env.PROSODY_ENABLE_RATE_LIMITS | default "0" | toBool -}}
+{{ $PUBLIC_URL := .Env.PUBLIC_URL | default "https://localhost:8443" -}}
+{{ $PUBLIC_URL_DOMAIN := $PUBLIC_URL | trimPrefix "https://" | trimSuffix "/" -}}
+{{ $TURN_HOST := .Env.TURN_HOST | default "" -}}
+{{ $TURN_HOSTS := splitList "," $TURN_HOST -}}
+{{ $TURN_PORT := .Env.TURN_PORT | default "443" -}}
+{{ $TURN_TRANSPORT := .Env.TURN_TRANSPORT | default "tcp" -}}
+{{ $TURN_TRANSPORTS := splitList "," $TURN_TRANSPORT -}}
+{{ $TURNS_HOST := .Env.TURNS_HOST | default "" -}}
+{{ $TURNS_HOSTS := splitList "," $TURNS_HOST -}}
+{{ $TURNS_PORT := .Env.TURNS_PORT | default "443" -}}
+{{ $XMPP_AUTH_DOMAIN := .Env.XMPP_AUTH_DOMAIN | default "auth.meet.jitsi" -}}
+{{ $XMPP_DOMAIN := .Env.XMPP_DOMAIN | default "meet.jitsi" -}}
+{{ $XMPP_GUEST_DOMAIN := .Env.XMPP_GUEST_DOMAIN | default "guest.meet.jitsi" -}}
+{{ $XMPP_INTERNAL_MUC_DOMAIN := .Env.XMPP_INTERNAL_MUC_DOMAIN | default "internal-muc.meet.jitsi" -}}
+{{ $XMPP_MUC_DOMAIN := .Env.XMPP_MUC_DOMAIN | default "muc.meet.jitsi" -}}
+{{ $XMPP_MUC_DOMAIN_PREFIX := (split "." $XMPP_MUC_DOMAIN)._0 -}}
+{{ $XMPP_RECORDER_DOMAIN := .Env.XMPP_RECORDER_DOMAIN | default "recorder.meet.jitsi" -}}
+{{ $JIBRI_RECORDER_USER := .Env.JIBRI_RECORDER_USER | default "recorder" -}}
+{{ $JIGASI_TRANSCRIBER_USER := .Env.JIGASI_TRANSCRIBER_USER | default "transcriber" -}}
+{{ $DISABLE_POLLS := .Env.DISABLE_POLLS | default "false" | toBool -}}
+{{ $ENABLE_SUBDOMAINS := .Env.ENABLE_SUBDOMAINS | default "true" | toBool -}}
+{{ $PROSODY_RESERVATION_ENABLED := .Env.PROSODY_RESERVATION_ENABLED | default "false" | toBool -}}
+{{ $PROSODY_RESERVATION_REST_BASE_URL := .Env.PROSODY_RESERVATION_REST_BASE_URL | default "" -}}
+{{ $RATE_LIMIT_LOGIN_RATE := .Env.PROSODY_RATE_LIMIT_LOGIN_RATE | default "3" -}}
+{{ $RATE_LIMIT_SESSION_RATE := .Env.PROSODY_RATE_LIMIT_SESSION_RATE | default "200" -}}
+{{ $RATE_LIMIT_TIMEOUT := .Env.PROSODY_RATE_LIMIT_TIMEOUT | default "60" -}}
+{{ $RATE_LIMIT_ALLOW_RANGES := .Env.PROSODY_RATE_LIMIT_ALLOW_RANGES | default "10.0.0.0/8" -}}
+{{ $RATE_LIMIT_CACHE_SIZE := .Env.PROSODY_RATE_LIMIT_CACHE_SIZE | default "10000" -}}
+{{ $ENV := .Env -}}
+
+admins = {
+    {{ if .Env.JIGASI_XMPP_PASSWORD }}
+    "{{ $JIGASI_XMPP_USER }}@{{ $XMPP_AUTH_DOMAIN }}",
+    {{ end }}
+
+    {{ if .Env.JIBRI_XMPP_PASSWORD }}
+    "{{ $JIBRI_XMPP_USER }}@{{ $XMPP_AUTH_DOMAIN }}",
+    {{ end }}
+
+    "focus@{{ $XMPP_AUTH_DOMAIN }}",
+    "{{ $JVB_AUTH_USER }}@{{ $XMPP_AUTH_DOMAIN }}"
+}
+
+unlimited_jids = {
+    "focus@{{ $XMPP_AUTH_DOMAIN }}",
+    "{{ $JVB_AUTH_USER }}@{{ $XMPP_AUTH_DOMAIN }}"
+}
+
+plugin_paths = { "/prosody-plugins/", "/prosody-plugins-custom" }
+
+muc_mapper_domain_base = "{{ $XMPP_DOMAIN }}";
+muc_mapper_domain_prefix = "{{ $XMPP_MUC_DOMAIN_PREFIX }}";
+
+http_default_host = "{{ $XMPP_DOMAIN }}"
+
+{{ if .Env.TURN_CREDENTIALS -}}
+external_service_secret = "{{.Env.TURN_CREDENTIALS}}";
+{{- end }}
+
+{{ if or .Env.TURN_HOST .Env.TURNS_HOST -}}
+external_services = {
+  {{ if $TURN_HOST -}}
+    {{- range $idx1, $host := $TURN_HOSTS -}}
+      {{- range $idx2, $transport := $TURN_TRANSPORTS -}}
+        {{- if or $idx1 $idx2 -}},{{- end }}
+        { type = "turn", host = "{{ $host }}", port = {{ $TURN_PORT }}, transport = "{{ $transport }}", secret = true, ttl = 86400, algorithm = "turn" }
+      {{- end -}}
+    {{- end -}}
+  {{- end -}}
+
+  {{- if $TURNS_HOST -}}
+    {{- range $idx, $host := $TURNS_HOSTS -}}
+        {{- if or $TURN_HOST $idx -}},{{- end }}
+        { type = "turns", host = "{{ $host }}", port = {{ $TURNS_PORT }}, transport = "tcp", secret = true, ttl = 86400, algorithm = "turn" }
+    {{- end }}
+  {{- end }}
+};
+{{- end }}
+
+{{ if and $ENABLE_AUTH (or (eq $PROSODY_AUTH_TYPE "jwt") (eq $PROSODY_AUTH_TYPE "hybrid_matrix_token")) .Env.JWT_ACCEPTED_ISSUERS }}
+asap_accepted_issuers = { "{{ join "\",\"" (splitList "," .Env.JWT_ACCEPTED_ISSUERS) }}" }
+{{ end }}
+
+{{ if and $ENABLE_AUTH (or (eq $PROSODY_AUTH_TYPE "jwt") (eq $PROSODY_AUTH_TYPE "hybrid_matrix_token")) .Env.JWT_ACCEPTED_AUDIENCES }}
+asap_accepted_audiences = { "{{ join "\",\"" (splitList "," .Env.JWT_ACCEPTED_AUDIENCES) }}" }
+{{ end }}
+
+consider_bosh_secure = true;
+consider_websocket_secure = true;
+
+{{ if $ENABLE_JAAS_COMPONENTS }}
+VirtualHost "jigasi.meet.jitsi"
+    modules_enabled = {
+      "ping";
+      "bosh";
+      "muc_password_check";
+    }
+    authentication = "token"
+    app_id = "jitsi";
+    asap_key_server = "https://jaas-public-keys.jitsi.net/jitsi-components/prod-8x8"
+    asap_accepted_issuers = { "jaas-components" }
+    asap_accepted_audiences = { "jigasi.{{ $PUBLIC_URL_DOMAIN }}" }
+{{ end }}
+
+VirtualHost "{{ $XMPP_DOMAIN }}"
+{{ if $ENABLE_AUTH }}
+  {{ if eq $PROSODY_AUTH_TYPE "jwt" }}
+    authentication = "{{ $JWT_AUTH_TYPE }}"
+    app_id = "{{ .Env.JWT_APP_ID }}"
+    app_secret = "{{ .Env.JWT_APP_SECRET }}"
+    allow_empty_token = {{ $JWT_ALLOW_EMPTY }}
+    {{ if $JWT_ASAP_KEYSERVER }}
+    asap_key_server = "{{ .Env.JWT_ASAP_KEYSERVER }}"
+    {{ end }}
+    enable_domain_verification = {{ $JWT_ENABLE_DOMAIN_VERIFICATION }}
+  {{ else if eq $PROSODY_AUTH_TYPE "ldap" }}
+    authentication = "cyrus"
+    cyrus_application_name = "xmpp"
+    allow_unencrypted_plain_auth = true
+  {{ else if eq $PROSODY_AUTH_TYPE "matrix" }}
+    authentication = "matrix_user_verification"
+    app_id = "{{ $MATRIX_UVS_ISSUER }}"
+    uvs_base_url = "{{ .Env.MATRIX_UVS_URL }}"
+    {{ if .Env.MATRIX_UVS_AUTH_TOKEN }}
+    uvs_auth_token = "{{ .Env.MATRIX_UVS_AUTH_TOKEN }}"
+    {{ end }}
+    {{ if $MATRIX_UVS_SYNC_POWER_LEVELS }}
+    uvs_sync_power_levels = true
+    {{ end }}
+  {{ else if eq $PROSODY_AUTH_TYPE "hybrid_matrix_token" }}
+    authentication = "hybrid_matrix_token"
+    app_id = "{{ .Env.JWT_APP_ID }}"
+    app_secret = "{{ .Env.JWT_APP_SECRET }}"
+    allow_empty_token = {{ $JWT_ALLOW_EMPTY }}
+    enable_domain_verification = {{ $JWT_ENABLE_DOMAIN_VERIFICATION }}
+
+    uvs_base_url = "{{ .Env.MATRIX_UVS_URL }}"
+    {{ if .Env.MATRIX_UVS_ISSUER }}
+    uvs_issuer = "{{ .Env.MATRIX_UVS_ISSUER }}"
+    {{ end }}
+    {{ if .Env.MATRIX_UVS_AUTH_TOKEN }}
+    uvs_auth_token = "{{ .Env.MATRIX_UVS_AUTH_TOKEN }}"
+    {{ end }}
+  {{ else if eq $PROSODY_AUTH_TYPE "internal" }}
+    authentication = "internal_hashed"
+  {{ end }}
+{{ else }}
+    authentication = "jitsi-anonymous"
+{{ end }}
+    ssl = {
+        key = "/config/certs/{{ $XMPP_DOMAIN }}.key";
+        certificate = "/config/certs/{{ $XMPP_DOMAIN }}.crt";
+    }
+    modules_enabled = {
+        "bosh";
+        {{ if $ENABLE_XMPP_WEBSOCKET }}
+        "websocket";
+        "smacks"; -- XEP-0198: Stream Management
+        {{ end }}
+        "pubsub";
+        "ping";
+        "speakerstats";
+        "conference_duration";
+        "room_metadata";
+        {{ if $ENABLE_END_CONFERENCE }}
+        "end_conference";
+        {{ end }}
+        {{ if or .Env.TURN_HOST .Env.TURNS_HOST }}
+        "external_services";
+        {{ end }}
+        {{ if $ENABLE_LOBBY }}
+        "muc_lobby_rooms";
+        {{ end }}
+        {{ if $ENABLE_BREAKOUT_ROOMS }}
+        "muc_breakout_rooms";
+        {{ end }}
+        {{ if $ENABLE_AV_MODERATION }}
+        "av_moderation";
+        {{ end }}
+        {{ if .Env.XMPP_MODULES }}
+        "{{ join "\";\n\"" (splitList "," .Env.XMPP_MODULES) }}";
+        {{ end }}
+        {{ if and $ENABLE_AUTH (eq $PROSODY_AUTH_TYPE "ldap") }}
+        "auth_cyrus";
+        {{end}}
+        {{ if $PROSODY_RESERVATION_ENABLED }}
+        "reservations";
+        {{ end }}
+    }
+
+    main_muc = "{{ $XMPP_MUC_DOMAIN }}"
+
+    {{ if $ENABLE_LOBBY }}
+    lobby_muc = "lobby.{{ $XMPP_DOMAIN }}"
+    {{ if $ENABLE_RECORDING }}
+    muc_lobby_whitelist = { "{{ $XMPP_RECORDER_DOMAIN }}" }
+    {{ end }}
+    {{ end }}
+
+    {{ if $PROSODY_RESERVATION_ENABLED }}
+    reservations_api_prefix = "{{ $PROSODY_RESERVATION_REST_BASE_URL }}"
+    {{ end }}
+
+    {{ if $ENABLE_BREAKOUT_ROOMS }}
+    breakout_rooms_muc = "breakout.{{ $XMPP_DOMAIN }}"
+    {{ end }}
+
+    speakerstats_component = "speakerstats.{{ $XMPP_DOMAIN }}"
+    conference_duration_component = "conferenceduration.{{ $XMPP_DOMAIN }}"
+
+    {{ if $ENABLE_END_CONFERENCE }}
+    end_conference_component = "endconference.{{ $XMPP_DOMAIN }}"
+    {{ end }}
+
+    {{ if $ENABLE_AV_MODERATION }}
+    av_moderation_component = "avmoderation.{{ $XMPP_DOMAIN }}"
+    {{ end }}
+
+    c2s_require_encryption = false
+
+{{ if $ENABLE_GUEST_DOMAIN }}
+VirtualHost "{{ $XMPP_GUEST_DOMAIN }}"
+    authentication = "jitsi-anonymous"
+
+    c2s_require_encryption = false
+{{ end }}
+
+VirtualHost "{{ $XMPP_AUTH_DOMAIN }}"
+    ssl = {
+        key = "/config/certs/{{ $XMPP_AUTH_DOMAIN }}.key";
+        certificate = "/config/certs/{{ $XMPP_AUTH_DOMAIN }}.crt";
+    }
+    modules_enabled = {
+        "limits_exception";
+    }
+    authentication = "internal_hashed"
+
+{{ if $ENABLE_RECORDING }}
+VirtualHost "{{ $XMPP_RECORDER_DOMAIN }}"
+    modules_enabled = {
+      "ping";
+    }
+    authentication = "internal_hashed"
+{{ end }}
+
+Component "{{ $XMPP_INTERNAL_MUC_DOMAIN }}" "muc"
+    storage = "memory"
+    modules_enabled = {
+        "ping";
+        {{ if .Env.XMPP_INTERNAL_MUC_MODULES -}}
+        "{{ join "\";\n\"" (splitList "," .Env.XMPP_INTERNAL_MUC_MODULES) }}";
+        {{ end -}}
+    }
+    restrict_room_creation = true
+    muc_room_locking = false
+    muc_room_default_public_jids = true
+
+Component "{{ $XMPP_MUC_DOMAIN }}" "muc"
+    restrict_room_creation = true
+    storage = "memory"
+    modules_enabled = {
+        "muc_meeting_id";
+        {{ if .Env.XMPP_MUC_MODULES -}}
+        "{{ join "\";\n\"" (splitList "," .Env.XMPP_MUC_MODULES) }}";
+        {{ end -}}
+        {{ if and $ENABLE_AUTH (or (eq $PROSODY_AUTH_TYPE "jwt") (eq $PROSODY_AUTH_TYPE "hybrid_matrix_token")) -}}
+        "{{ $JWT_TOKEN_AUTH_MODULE }}";
+        {{ end }}
+        {{ if and $ENABLE_AUTH (eq $PROSODY_AUTH_TYPE "matrix") $MATRIX_UVS_SYNC_POWER_LEVELS -}}
+        "matrix_power_sync";
+        {{ end -}}
+        {{ if and $ENABLE_AUTH (eq $PROSODY_AUTH_TYPE "hybrid_matrix_token") $MATRIX_UVS_SYNC_POWER_LEVELS -}}
+        "matrix_affiliation";
+        {{ end -}}
+        {{ if not $DISABLE_POLLS -}}
+        "polls";
+        {{ end -}}
+        {{ if $ENABLE_SUBDOMAINS -}}
+        "muc_domain_mapper";
+        {{ end -}}
+        {{ if $ENABLE_RATE_LIMITS -}}
+        "muc_rate_limit";
+        "rate_limit";
+        {{ end -}}
+        {{ if .Env.MAX_PARTICIPANTS }}
+        "muc_max_occupants";
+        {{ end }}
+        "muc_password_whitelist";
+    }
+
+    {{ if $ENABLE_RATE_LIMITS -}}
+    -- Max allowed join/login rate in events per second.
+	rate_limit_login_rate = {{ $RATE_LIMIT_LOGIN_RATE }};
+	-- The rate to which sessions from IPs exceeding the join rate will be limited, in bytes per second.
+	rate_limit_session_rate = {{ $RATE_LIMIT_SESSION_RATE }};
+	-- The time in seconds, after which the limit for an IP address is lifted.
+	rate_limit_timeout = {{ $RATE_LIMIT_TIMEOUT }};
+	-- List of regular expressions for IP addresses that are not limited by this module.
+	rate_limit_whitelist = {
+      "127.0.0.1";
+      {{ range $index, $cidr := (splitList "," $RATE_LIMIT_ALLOW_RANGES) -}}
+      "{{ $cidr }}";
+      {{ end -}}
+    };
+
+    rate_limit_whitelist_jids = {
+        "{{ $JIBRI_RECORDER_USER }}@{{ $XMPP_RECORDER_DOMAIN }}",
+        "{{ $JIGASI_TRANSCRIBER_USER }}@{{ $XMPP_RECORDER_DOMAIN }}"
+    }
+    {{ end -}}
+
+	-- The size of the cache that saves state for IP addresses
+	rate_limit_cache_size = {{ $RATE_LIMIT_CACHE_SIZE }};
+
+    muc_room_cache_size = 1000
+    muc_room_locking = false
+    muc_room_default_public_jids = true
+    {{ if .Env.XMPP_MUC_CONFIGURATION -}}
+    {{ join "\n" (splitList "," .Env.XMPP_MUC_CONFIGURATION) }}
+    {{ end -}}
+    {{ if .Env.MAX_PARTICIPANTS }}
+    muc_access_whitelist = { "focus@{{ .Env.XMPP_AUTH_DOMAIN }}" }
+    muc_max_occupants = "{{ .Env.MAX_PARTICIPANTS }}"
+    {{ end }}
+    muc_password_whitelist = {
+        "focus@{{ .Env.XMPP_AUTH_DOMAIN }}"
+    }
+
+Component "focus.{{ $XMPP_DOMAIN }}" "client_proxy"
+    target_address = "focus@{{ $XMPP_AUTH_DOMAIN }}"
+
+Component "speakerstats.{{ $XMPP_DOMAIN }}" "speakerstats_component"
+    muc_component = "{{ $XMPP_MUC_DOMAIN }}"
+
+Component "conferenceduration.{{ $XMPP_DOMAIN }}" "conference_duration_component"
+    muc_component = "{{ $XMPP_MUC_DOMAIN }}"
+
+{{ if $ENABLE_END_CONFERENCE }}
+Component "endconference.{{ $XMPP_DOMAIN }}" "end_conference"
+    muc_component = "{{ $XMPP_MUC_DOMAIN }}"
+{{ end }}
+
+{{ if $ENABLE_AV_MODERATION }}
+Component "avmoderation.{{ $XMPP_DOMAIN }}" "av_moderation_component"
+    muc_component = "{{ $XMPP_MUC_DOMAIN }}"
+{{ end }}
+
+{{ if $ENABLE_LOBBY }}
+Component "lobby.{{ $XMPP_DOMAIN }}" "muc"
+    storage = "memory"
+    restrict_room_creation = true
+    muc_room_locking = false
+    muc_room_default_public_jids = true
+    modules_enabled = {
+        {{ if $ENABLE_RATE_LIMITS -}}
+        "muc_rate_limit";
+        {{ end -}}
+    }
+
+    {{ end }}
+
+{{ if $ENABLE_BREAKOUT_ROOMS }}
+Component "breakout.{{ $XMPP_DOMAIN }}" "muc"
+    storage = "memory"
+    restrict_room_creation = true
+    muc_room_locking = false
+    muc_room_default_public_jids = true
+    modules_enabled = {
+        "muc_meeting_id";
+        {{ if $ENABLE_SUBDOMAINS -}}
+        "muc_domain_mapper";
+        {{ end -}}
+        {{ if not $DISABLE_POLLS -}}
+        "polls";
+        {{ end -}}
+        {{ if $ENABLE_RATE_LIMITS -}}
+        "muc_rate_limit";
+        {{ end -}}
+    }
+{{ end }}
+
+Component "metadata.{{ $XMPP_DOMAIN }}" "room_metadata_component"
+    muc_component = "{{ $XMPP_MUC_DOMAIN }}"
+    breakout_rooms_component = "breakout.{{ $XMPP_DOMAIN }}"
diff --git a/charts/sovereign-workplace-jitsi/files/prosody/prosody-plugins-custom/mod_auth_hybrid_matrix_token.lua b/charts/sovereign-workplace-jitsi/files/prosody/prosody-plugins-custom/mod_auth_hybrid_matrix_token.lua
new file mode 100644
index 0000000000000000000000000000000000000000..5130008c38afde72e8c46cdd50327e1c4f79055f
--- /dev/null
+++ b/charts/sovereign-workplace-jitsi/files/prosody/prosody-plugins-custom/mod_auth_hybrid_matrix_token.lua
@@ -0,0 +1,364 @@
+-- SPDX-FileCopyrightText: 2023 Bundesministerium des Innern und für Heimat, PG ZenDiS "Projektgruppe für Aufbau ZenDiS"
+-- SPDX-License-Identifier: Apache-2.0
+-- -----------------------------------------------------------------------------
+-- Hybrid Matrix-Token authentication
+-- -----------------------------------------------------------------------------
+-- This module is an authentication provider for Prosody which supports Matrix
+-- and standard Jitsi token at the same time. It senses the type of the token
+-- and handles it depending on its type.
+-- -----------------------------------------------------------------------------
+local async = require "util.async"
+local basexx = require 'basexx'
+local cjson_safe  = require 'cjson.safe'
+local formdecode = require "util.http".formdecode
+local generate_uuid = require "util.uuid".generate
+local http = require "net.http"
+local json = require "util.json"
+local new_sasl = require "util.sasl".new
+local sasl = require "util.sasl"
+local sessions = prosody.full_sessions
+local token_util = module:require "token/util".new(module)
+
+-- no token configuration
+if token_util == nil then
+    return
+end
+
+module:depends("jitsi_session")
+
+local uvsIssuer = {"*"}
+local issuer = module:get_option("uvs_issuer", nil)
+if issuer then
+    uvsIssuer = { string.format("%s", issuer) }
+end
+
+local uvsUrl = module:get_option("uvs_base_url", nil)
+if uvsUrl == nil then
+    module:log("warn", "Missing 'uvs_base_url' config")
+end
+
+local uvsAuthToken = module:get_option("uvs_auth_token", nil)
+if uvsAuthToken == nil then
+    module:log(
+        "info",
+        "No uvs_auth_token supplied, not sending authentication headers"
+    )
+end
+
+-- define auth provider
+local provider = {}
+
+local host = module.host
+
+-- Extract 'token' and 'room' params from URL when session is created
+function init_session(event)
+    local session, request = event.session, event.request
+    local query = request.url.query
+
+    if query ~= nil then
+        local params = formdecode(query)
+
+        session.auth_token = params and params.token or nil
+        session.jitsi_room = params and params.room or nil
+    end
+end
+
+module:hook_global("bosh-session", init_session)
+module:hook_global("websocket-session", init_session)
+
+function provider.test_password(_username, _password)
+    return nil, "Password based auth not supported"
+end
+
+function provider.get_password(_username)
+    return nil
+end
+
+function provider.set_password(_username, _password)
+    return nil, "Set password not supported"
+end
+
+function provider.user_exists(_username)
+    return nil
+end
+
+function provider.create_user(_username, _password)
+    return nil
+end
+
+function provider.delete_user(_username)
+    return nil
+end
+
+local function split_token(token)
+    local segments = {}
+    for seg in string.gmatch(token, "([^.]+)") do
+        table.insert(segments, seg)
+    end
+
+    return segments
+end
+
+local function parse_token(token)
+    if type(token) ~= "string" then return nil, nil, nil end
+
+    local segments = split_token(token)
+    if #segments ~= 3 then return nil, nil, nil end
+
+    local header, err1 = cjson_safe.decode(basexx.from_url64(segments[1]))
+    if err1 then return nil, nil, nil end
+
+    local payload, err2 = cjson_safe.decode(basexx.from_url64(segments[2]))
+    if err2 then return nil, nil, nil end
+
+    local sign, err3 = basexx.from_url64(segments[3])
+    if err3 then return nil, nil, nil end
+
+    return header, payload, sign
+end
+
+local function get_options(matrixPayload)
+    local options = {}
+
+    options.method = "POST"
+
+    options.headers = {
+        ["Content-Type"] = "application/json",
+        ["User-Agent"] = string.format("Prosody (%s)", prosody.version)
+    }
+    if uvsAuthToken ~= nil then
+        options.headers["Authorization"] = string.format(
+            "Bearer %s", uvsAuthToken
+        )
+    end
+
+    local body = {
+        ["token"] = matrixPayload["token"],
+        ["room_id"] = matrixPayload["room_id"]
+    }
+    if matrixPayload.server_name then
+        body["matrix_server_name"] = matrixPayload.server_name
+    end
+    options.body = json.encode(body)
+
+    return options
+end
+
+local function is_user_in_room(session, matrixPayload)
+    local url = string.format("%s/verify/user_in_room", uvsUrl)
+    local options = get_options(matrixPayload)
+    local wait, done = async.waiter()
+    local httpRes
+
+    local function cb(resBody, resCode, _req, _res)
+        if resCode == 200 then
+            httpRes = json.decode(resBody)
+        end
+
+        done()
+    end
+
+    http.request(url, options, cb)
+    wait()
+
+    -- no result
+    if not (httpRes and httpRes.results) then
+        return false
+    end
+
+    -- not a member of Matrix room
+    if not (httpRes.results.user and httpRes.results.room_membership) then
+        return false
+    end
+
+    -- set affiliation as session value according to their power level
+    session.matrix_affiliation = "member"
+    session.auth_matrix_user_verification_is_owner = false
+    if
+        httpRes.power_levels and httpRes.power_levels.user and
+        httpRes.power_levels.room and httpRes.power_levels.room.state_default and
+        httpRes.power_levels.user >= httpRes.power_levels.room.state_default
+    then
+        session.matrix_affiliation = "owner"
+        session.auth_matrix_user_verification_is_owner = true
+    end
+
+    return true
+end
+
+local function matrix_handler(session, payload)
+    if uvsUrl == nil then
+        module:log("warn", "Missing 'uvs_base_url' config")
+        session.auth_token = nil
+        return false, "access-denied", "missing Matrix UVS address"
+    end
+
+    session.public_key = "notused"
+    local res, error, reason = token_util:process_and_verify_token(
+        session,
+        uvsIssuer
+    )
+    if res == false then
+        module:log(
+            "warn",
+            "Error verifying token err:%s, reason:%s", error, reason
+        )
+        session.auth_token = nil
+        return res, error, reason
+    end
+
+    if payload.context.matrix.room_id == nil then
+        module:log("warn", "Missing Matrix room_id in token")
+        session.auth_token = nil
+        return false, "bad-request", "Matrix room ID must be given"
+    end
+
+    local decodedRoomId = basexx.from_base32(session.jitsi_room)
+    if decodedRoomId ~= payload.context.matrix.room_id then
+        module:log("warn", "Jitsi and Matrix rooms don't match")
+        session.auth_token = nil
+        return false, "access-denied", "Jitsi room does not match Matrix room"
+    end
+
+    if not is_user_in_room(session, payload.context.matrix) then
+        module:log("warn", "Matrix token is invalid or user does not in room")
+        session.auth_token = nil
+        return false, "access-denied", "Matrix token invalid or not in room"
+    end
+
+    session.jitsi_meet_context_matrix = payload.context.matrix
+
+    return true, nil, nil
+end
+
+local function token_handler(session)
+    -- retrieve custom public key from server and save it on the session
+    local preEvent = prosody.events.fire_event(
+        "pre-jitsi-authentication-fetch-key",
+        session
+    )
+    if preEvent ~= nil and preEvent.res == false then
+        module:log(
+            "warn",
+            "Error verifying token on pre authentication stage:%s, reason:%s",
+                preEvent.error,
+                preEvent.reason
+        )
+        session.auth_token = nil
+        return preEvent.res, preEvent.error, preEvent.reason
+    end
+
+    local res, error, reason = token_util:process_and_verify_token(session)
+    if res == false then
+        module:log(
+            "warn",
+            "Error verifying token err:%s, reason:%s", error, reason
+        )
+        session.auth_token = nil
+        return res, error, reason
+    end
+
+    return true, nil, nil
+end
+
+local function common_handler(self, session, message)
+    local shouldAllow = prosody.events.fire_event(
+        "jitsi-access-ban-check",
+        session
+    )
+    if shouldAllow == false then
+        module:log("warn", "user is banned")
+        return false, "not-allowed", "user is banned"
+    end
+
+    local customUsername = prosody.events.fire_event(
+        "pre-jitsi-authentication",
+        session
+    )
+
+    if (customUsername) then
+        self.username = customUsername
+    elseif (session.previd ~= nil) then
+        for _, s in pairs(sessions) do
+            if (s.resumption_token == session.previd) then
+                self.username = s.username
+                break
+            end
+        end
+    else
+        self.username = message
+    end
+
+    local postEvent = prosody.events.fire_event(
+        "post-jitsi-authentication",
+        session
+    )
+    if postEvent ~= nil and postEvent.res == false then
+        module:log(
+            "warn",
+            "Error verifying token on post authentication :%s, reason:%s",
+            postEvent.error,
+            postEvent.reason
+        )
+        session.auth_token = nil
+        return postEvent.res, postEvent.error, postEvent.reason
+    end
+
+    return true, nil, nil
+end
+
+function provider.get_sasl_handler(session)
+    local function handler(self, message)
+        local payload
+        if session.auth_token then
+            _, payload, _ = parse_token(session.auth_token)
+        end
+
+        if payload and payload.context and payload.context.matrix then
+            module:log("info", "Matrix authentication handler is selected")
+
+            local res, error, reason = matrix_handler(session, payload)
+            if res == false then
+                return res, error, reason
+            end
+        else
+            module:log("info", "Token authentication handler is selected")
+
+            local res, error, reason = token_handler(session)
+            if res == false then
+                return res, error, reason
+            end
+        end
+
+        local res, error, reason = common_handler(self, session, message)
+        if res == false then
+            return res, error, reason
+        end
+
+        return true
+    end
+
+    return new_sasl(host, { anonymous = handler })
+end
+
+module:provides("auth", provider)
+
+local function anonymous(self, message)
+    module:log("debug", "Message in anonymous: %s", message)
+
+    local username = generate_uuid()
+
+    -- This calls the handler created in 'provider.get_sasl_handler(session)'
+    local result, err, msg = self.profile.anonymous(self, username, self.realm)
+
+    if result == true then
+        if (self.username == nil) then
+            self.username = username
+        end
+        return "success"
+    else
+        return "failure", err, msg
+    end
+end
+
+sasl.registerMechanism("ANONYMOUS", {"anonymous"}, anonymous)
diff --git a/charts/sovereign-workplace-jitsi/files/prosody/prosody-plugins-custom/mod_matrix_affiliation.lua b/charts/sovereign-workplace-jitsi/files/prosody/prosody-plugins-custom/mod_matrix_affiliation.lua
new file mode 100644
index 0000000000000000000000000000000000000000..eb17ba7f7f6a25445a2b247e3d6cd7313cfc0d83
--- /dev/null
+++ b/charts/sovereign-workplace-jitsi/files/prosody/prosody-plugins-custom/mod_matrix_affiliation.lua
@@ -0,0 +1,78 @@
+-- SPDX-FileCopyrightText: 2023 Bundesministerium des Innern und für Heimat, PG ZenDiS "Projektgruppe für Aufbau ZenDiS"
+-- SPDX-License-Identifier: Apache-2.0
+-- -----------------------------------------------------------------------------
+-- Matrix Affiliation (downgrade only)
+-- -----------------------------------------------------------------------------
+-- This module updates the affiliation of participants if the room is created
+-- by Element's Jitsi widget. This module checks Jitsi room name to understand
+-- if this room is created by widget or not...
+--
+-- base32.decode(jitsi_room_name) should match "!.*:.*[.].*" (regex) for related
+-- rooms.
+--
+-- This module assumes that the authentication is already enabled on Jicofo. So
+-- every participants who have a valid token will become moderator (owner) by
+-- default (this is not what we want).
+--
+-- This module downgrades the affiliation level (from owner to member) of
+-- the participant if she is not an admin in the related Matrix room.
+-- -----------------------------------------------------------------------------
+local basexx = require 'basexx'
+local is_admin = require "core.usermanager".is_admin
+local is_healthcheck_room = module:require "util".is_healthcheck_room
+local jid_split = require "util.jid".split
+local timer = require "util.timer"
+local LOGLEVEL = "debug"
+
+local function _is_admin(jid)
+    return is_admin(jid, module.host)
+end
+
+module:hook("muc-occupant-joined", function (event)
+    local room, occupant = event.room, event.occupant
+
+    if is_healthcheck_room(room.jid) or _is_admin(occupant.jid) then
+        module:log(LOGLEVEL, "skip affiliation, %s", occupant.jid)
+        return
+    end
+
+    if not event.origin.auth_token then
+        module:log(LOGLEVEL, "skip affiliation, no token")
+        return
+    end
+
+    local roomName, _ = jid_split(room.jid)
+    local roomId = basexx.from_base32(roomName)
+    if not roomId then
+        module:log(LOGLEVEL, "skip affiliation, cannot decode the room name")
+        return
+    end
+
+    local isMatrixRoom = string.match(roomId, "!.*:.*[.].*")
+    if not isMatrixRoom then
+        module:log(LOGLEVEL, "skip affiliation, not a Matrix room")
+        return
+    end
+
+    if event.origin.matrix_affiliation == "owner" then
+        module:log(LOGLEVEL, "skip downgrading, valid Matrix owner")
+        return
+    end
+
+    -- All users who have a valid token are set as owner by jicofo when
+    -- auhentication is enabled on jicofo. Downgrade the affiliation for all
+    -- users who are not a Matrix owner (even they have a valid token).
+    -- A timer is used because Jicofo will update the affiliation after this
+    -- internal authentication phase is completed. It should be overwritten.
+    local i = 0.0
+    while (i < 2.0) do
+        timer.add_task(i, function()
+            room:set_affiliation(true, occupant.bare_jid, "member")
+        end)
+        i = i + 0.2
+    end
+    module:log( "info",
+	"affiliation is downgraded, occupant: %s",
+	occupant.bare_jid
+    )
+end)
diff --git a/charts/sovereign-workplace-jitsi/templates/configmap.yaml b/charts/sovereign-workplace-jitsi/templates/configmap.yaml
index 936def0cc444a65eae992fa1d5746f6b9156389d..9f729a5312c9a7e94e8c52fa86b2eb4f3467260f 100644
--- a/charts/sovereign-workplace-jitsi/templates/configmap.yaml
+++ b/charts/sovereign-workplace-jitsi/templates/configmap.yaml
@@ -50,4 +50,18 @@ data:
     {{- end }}
     kubectl rollout restart deployment jitsi-jvb
     {{- end }}
+---
+kind: "ConfigMap"
+apiVersion: "v1"
+metadata:
+  name: "prosody-swp"
+data:
+{{ (.Files.Glob "files/prosody/jitsi-meet.cfg.lua").AsConfig | nindent 2 }}
+---
+kind: "ConfigMap"
+apiVersion: "v1"
+metadata:
+  name: "prosody-plugins-swp"
+data:
+{{ (.Files.Glob "files/prosody/prosody-plugins-custom/*").AsConfig | nindent 2 }}
 ...
diff --git a/charts/sovereign-workplace-jitsi/values.yaml b/charts/sovereign-workplace-jitsi/values.yaml
index 817b985511c8d59e2a97c2ab1fd6fb3f43b3792b..6b50fb7f31d34b4f2604975e0120a0c6c2027955 100644
--- a/charts/sovereign-workplace-jitsi/values.yaml
+++ b/charts/sovereign-workplace-jitsi/values.yaml
@@ -176,7 +176,7 @@ jitsi:
   web:
     replicaCount: 1
     image:
-      tag: "stable-8615"
+      tag: "stable-8719"
       pullPolicy: "IfNotPresent"
     extraVolumes:
       - name: "jitsi-meet-swp"
@@ -222,18 +222,36 @@ jitsi:
 
   prosody:
     image:
-      tag: "stable-8615"
+      tag: "stable-8719"
       pullPolicy: "IfNotPresent"
     extraEnvs:
       - name: "AUTH_TYPE"
         value: "jwt"
+    extraVolumes:
+      - name: "prosody-swp"
+        configMap:
+          name: "prosody-swp"
+          items:
+            - key: "jitsi-meet.cfg.lua"
+              path: "jitsi-meet.cfg.lua"
+      - name: "prosody-plugins-custom"
+        configMap:
+          name: "prosody-plugins-swp"
+    extraVolumeMounts:
+      - name: "prosody-swp"
+        mountPath: "/defaults/conf.d/jitsi-meet.cfg.lua"
+        subPath: "jitsi-meet.cfg.lua"
+      - name: "prosody-plugins-custom"
+        mountPath: "/prosody-plugins-custom"
 
   jicofo:
     image:
-      tag: "stable-8615"
+      tag: "stable-8719"
       pullPolicy: "IfNotPresent"
     extraEnvs:
       AUTH_TYPE: "xmpp"
+      ENABLE_AUTO_LOGIN: "false"
+      JICOFO_AUTH_LIFETIME: "100 milliseconds"
     extraEnvFrom:
       - secretRef:
           name: "{{ include 'prosody.fullname' . }}-jicofo"
@@ -246,9 +264,9 @@ jitsi:
 
   jvb:
     image:
-      tag: "stable-8615"
+      tag: "stable-8719"
       pullPolicy: "IfNotPresent"
-    replicaCount: 2
+    replicaCount: 1
     service:
       externalTrafficPolicy: ""
       enabled: true
@@ -259,7 +277,7 @@ jitsi:
     recording: true
     livestreaming: true
     image:
-      tag: "stable-8615"
+      tag: "stable-8719"
       pullPolicy: "IfNotPresent"
     readinessProbe:
       initialDelaySeconds: 15