diff --git a/debian/control b/debian/control index 5633ae9ee..47d274ce9 100644 --- a/debian/control +++ b/debian/control @@ -32,3 +32,9 @@ Description: Prosody configuration for Jitsi Meet . This package contains configuration for Prosody to be used with Jitsi Meet. + +Package: jitsi-meet-tokens +Architecture: all +Depends: ${misc:Depends}, prosody | prosody-trunk, jitsi-meet-prosody +Description: Prosody token authentication plugin for Jitsi Meet + diff --git a/debian/jitsi-meet-tokens.README.Debian b/debian/jitsi-meet-tokens.README.Debian new file mode 100644 index 000000000..0a54726f4 --- /dev/null +++ b/debian/jitsi-meet-tokens.README.Debian @@ -0,0 +1,7 @@ +Token authentication plugin for Jitsi Meet +---------------------------- + +Jitsi Meet is a WebRTC video conferencing application. This package contains +Prosody plugin which enables token authentication in Jitsi Meet installation. + + -- Pawel Domas Mon, 2 Nov 2015 14:45:00 -0600 diff --git a/debian/jitsi-meet-tokens.config b/debian/jitsi-meet-tokens.config new file mode 100644 index 000000000..db965ce2c --- /dev/null +++ b/debian/jitsi-meet-tokens.config @@ -0,0 +1,10 @@ +#!/bin/sh -e + +# Source debconf library. +. /usr/share/debconf/confmodule + +db_input critical jitsi-meet-tokens/appid || true +db_go + +db_input critical jitsi-meet-tokens/appsecret || true +db_go \ No newline at end of file diff --git a/debian/jitsi-meet-tokens.docs b/debian/jitsi-meet-tokens.docs new file mode 100644 index 000000000..e69de29bb diff --git a/debian/jitsi-meet-tokens.install b/debian/jitsi-meet-tokens.install new file mode 100644 index 000000000..189b92064 --- /dev/null +++ b/debian/jitsi-meet-tokens.install @@ -0,0 +1 @@ +prosody-plugins/ /usr/share/jitsi-meet/ diff --git a/debian/jitsi-meet-tokens.postinst b/debian/jitsi-meet-tokens.postinst new file mode 100644 index 000000000..f9c4901ba --- /dev/null +++ b/debian/jitsi-meet-tokens.postinst @@ -0,0 +1,91 @@ +#!/bin/bash +# postinst script for jitsi-meet-tokens +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `configure' +# * `abort-upgrade' +# * `abort-remove' `in-favour' +# +# * `abort-remove' +# * `abort-deconfigure' `in-favour' +# `removing' +# +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + + +case "$1" in + configure) + + if [ -f "/etc/jitsi/videobridge/config" ] ; then + . /etc/jitsi/videobridge/config + fi + + if [ -f "/etc/jitsi/jicofo/config" ] ; then + . /etc/jitsi/jicofo/config + fi + + # loading debconf + . /usr/share/debconf/confmodule + + db_get jitsi-meet-tokens/appid + if [ "$RET" = "false" ] ; then + echo "Application ID is mandatory" + exit 1 + fi + APP_ID=$RET + + db_get jitsi-meet-tokens/appsecret + if [ "$RET" = "false" ] ; then + echo "Application secret is mandatory" + fi + APP_SECRET=$RET + + # We can adjust Prosody config only if there is Jvb or Jicofo domain configured + PROSODY_HOST_CONFIG="/etc/prosody/conf.avail/$JVB_HOSTNAME.cfg.lua" + if [ ! -f "$PROSODY_HOST_CONFIG" ] ; then + PROSODY_HOST_CONFIG="/etc/prosody/conf.avail/$JICOFO_HOSTNAME.cfg.lua" + fi + + # Store config filename for purge + db_set jitsi-meet-prosody/prosody_config $PROSODY_HOST_CONFIG + + db_stop + + if [ -f "$PROSODY_HOST_CONFIG" ] ; then + if grep -q "--plugin_paths" "$PROSODY_HOST_CONFIG"; then + # enable tokens in prosody host config + sed -i "s/--plugin_paths/plugin_paths/g" $PROSODY_HOST_CONFIG + sed -i 's/authentication = "anonymous"/authentication = "token"/g' $PROSODY_HOST_CONFIG + sed -i "s/--app_id=example_app_id/app_id=$APP_ID/g" $PROSODY_HOST_CONFIG + sed -i "s/--app_secret=example_app_secret/app_secret=$APP_SECRET/g" $PROSODY_HOST_CONFIG + sed -i 's/--modules_enabled = { "token_verification" }/modules_enabled = { "token_verification" }/g' $PROSODY_HOST_CONFIG + + else + echo "Failed apply auto-config to $PROSODY_HOST_CONFIG which most likely comes from not supported version of jitsi-meet" + fi + else + echo "Prosody config not found at $PROSODY_HOST_CONFIG - unable to auto-configure token authentication" + fi + + ;; + + abort-upgrade|abort-remove|abort-deconfigure) + ;; + + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +exit 0 diff --git a/debian/jitsi-meet-tokens.postrm b/debian/jitsi-meet-tokens.postrm new file mode 100644 index 000000000..acb1845cf --- /dev/null +++ b/debian/jitsi-meet-tokens.postrm @@ -0,0 +1,73 @@ +#!/bin/sh +# postrm script for jitsi-meet-tokens +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `remove' +# * `purge' +# * `upgrade' +# * `failed-upgrade' +# * `abort-install' +# * `abort-install' +# * `abort-upgrade' +# * `disappear' +# +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + +# Load debconf +. /usr/share/debconf/confmodule + + +case "$1" in + remove) + + db_get jitsi-meet-prosody/prosody_config + PROSODY_HOST_CONFIG=$RET + + if [ -f "$PROSODY_HOST_CONFIG" ] ; then + + db_get jitsi-meet-tokens/appid + APP_ID=$RET + + db_get jitsi-meet-tokens/appsecret + APP_SECRET=$RET + + # Revert prosody config + sed -i "s/plugin_paths/--plugin_paths/g" $PROSODY_HOST_CONFIG + sed -i 's/authentication = "token"/authentication = "anonymous"/g' $PROSODY_HOST_CONFIG + sed -i "s/app_id=$APP_ID/--app_id=example_app_id/g" $PROSODY_HOST_CONFIG + sed -i "s/app_secret=$APP_SECRET/--app_secret=example_app_secret/g" $PROSODY_HOST_CONFIG + sed -i 's/modules_enabled = { "token_verification" }/--modules_enabled = { "token_verification" }/g' $PROSODY_HOST_CONFIG + + if [ -x "/etc/init.d/prosody" ]; then + invoke-rc.d prosody reload + fi + fi + + db_stop + ;; + + purge) + ;; + + upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) + ;; + + *) + echo "postrm called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +db_stop + +exit 0 diff --git a/debian/jitsi-meet-tokens.templates b/debian/jitsi-meet-tokens.templates new file mode 100644 index 000000000..de31185f5 --- /dev/null +++ b/debian/jitsi-meet-tokens.templates @@ -0,0 +1,14 @@ +Template: jitsi-meet-tokens/appid +Type: string +_Description: The application ID to be used by token authentication plugin: + Application ID: + +Template: jitsi-meet-tokens/appsecret +Type: password +_Description: The application secret to be used by token authentication plugin: + Application secret: + +Template: jitsi-meet-prosody/prosody_config +Type: string +_Description: The location of Jitsi Meet Prosody config file + Jitsi-meet Prosody config file location: \ No newline at end of file diff --git a/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example b/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example index e2a132009..7bce5167f 100644 --- a/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example +++ b/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example @@ -1,6 +1,13 @@ +-- Plugins path gets uncommented during jitsi-meet-tokens package install - that's where token plugin is located +--plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" } + VirtualHost "jitmeet.example.com" -- enabled = false -- Remove this line to enable this host authentication = "anonymous" + -- Two properties below get uncommented by jitsi-meet-tokens package config + -- and authentication above is switched to "token" + --app_id=example_app_id + --app_secret=example_app_secret -- Assign this host a certificate for TLS, otherwise it would use the one -- set in the global section (if any). -- Note that old-style SSL on port 5223 only supports one certificate, and will always @@ -17,13 +24,14 @@ VirtualHost "jitmeet.example.com" } Component "conference.jitmeet.example.com" "muc" + --modules_enabled = { "token_verification" } admins = { "focusUser@auth.jitmeet.example.com" } Component "jitsi-videobridge.jitmeet.example.com" component_secret = "jitmeetSecret" VirtualHost "auth.jitmeet.example.com" - authentication = "internal_plain" + authentication = "internal_plain" Component "focus.jitmeet.example.com" component_secret = "focusSecret" diff --git a/prosody-plugins/mod_auth_token.lua b/prosody-plugins/mod_auth_token.lua new file mode 100644 index 000000000..bf2df1920 --- /dev/null +++ b/prosody-plugins/mod_auth_token.lua @@ -0,0 +1,75 @@ +-- Token authentication +-- Copyright (C) 2015 Atlassian + +local usermanager = require "core.usermanager"; +local new_sasl = require "util.sasl".new; + +local log = module._log; +local host = module.host; + +local token_util = module:require "token/util"; + +-- define auth provider +local provider = {}; + +--do +-- local list; +-- for mechanism in pairs(new_sasl(module.host):mechanisms()) do +-- list = (not(list) and mechanism) or (list..", "..mechanism); +-- end +-- if not list then +-- module:log("error", "No mechanisms"); +-- else +-- module:log("error", "Mechanisms: %s", list); +-- end +--end + + +local appId = module:get_option_string("app_id"); +local appSecret = module:get_option_string("app_secret"); +local tokenLifetime = module:get_option_number("token_lifetime"); + +function provider.test_password(username, password) + local result, msg = token_util.verify_password(password, appId, appSecret, tokenLifetime); + if result == true then + return true; + else + log("error", "Token auth failed for user %s, reason: %s",username, msg); + return nil, msg; + end +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.users() + return next, hosts[module.host].sessions, nil; +end + +function provider.create_user(username, password) + return nil; +end + +function provider.delete_user(username) + return nil; +end + +function provider.get_sasl_handler() + local testpass_authentication_profile = { + plain_test = function(sasl, username, password, realm) + return usermanager.test_password(username, realm, password), true; + end + }; + return new_sasl(host, testpass_authentication_profile); +end + +module:provides("auth", provider); diff --git a/prosody-plugins/mod_token_verification.lua b/prosody-plugins/mod_token_verification.lua new file mode 100644 index 000000000..08e1e53ec --- /dev/null +++ b/prosody-plugins/mod_token_verification.lua @@ -0,0 +1,52 @@ +-- Token authentication +-- Copyright (C) 2015 Atlassian + +local log = module._log; +local host = module.host; +local st = require "util.stanza"; +local token_util = module:require("token/util"); +local is_admin = require "core.usermanager".is_admin; + + +local parentHostName = string.gmatch(tostring(host), "%w+.(%w.+)")(); +if parentHostName == nil then + log("error", "Failed to start - unable to get parent hostname"); + return; +end + +local parentCtx = module:context(parentHostName); +if parentCtx == nil then + log("error", "Failed to start - unable to get parent context for host: %s", tostring(parentHostName)); + return; +end + +local appId = parentCtx:get_option_string("app_id"); +local appSecret = parentCtx:get_option_string("app_secret"); +local tokenLifetime = parentCtx:get_option_string("token_lifetime"); + +log("debug", "%s - starting MUC token verifier app_id: %s app_secret: %s token-lifetime: %s", + tostring(host), tostring(appId), tostring(appSecret), tostring(tokenLifetime)); + +local function handle_pre_create(event) + local origin, stanza = event.origin, event.stanza; + local token = stanza:get_child("token", "http://jitsi.org/jitmeet/auth-token"); + -- token not required for admin users + local user_jid = stanza.attr.from; + if is_admin(user_jid) then + log("debug", "Token not required from admin user: %s", user_jid); + return nil; + end + log("debug", "Will verify token for user: %s ", user_jid); + if token ~= nil then + token = token[1]; + end + local result, msg = token_util.verify_password(token, appId, appSecret, tokenLifetime); + if result ~= true then + log("debug", "Token verification failed: %s", msg); + origin.send(st.error_reply(stanza, "cancel", "not-allowed", msg)); + return true; + end +end + +module:hook("muc-room-pre-create", handle_pre_create); + diff --git a/prosody-plugins/token/util.lib.lua b/prosody-plugins/token/util.lib.lua new file mode 100644 index 000000000..64747e03a --- /dev/null +++ b/prosody-plugins/token/util.lib.lua @@ -0,0 +1,76 @@ +-- Token authentication +-- Copyright (C) 2015 Atlassian + +local hashes = require "util.hashes"; + +local _M = {}; + +local function calc_hash(password, appId, appSecret) + local hash, room, ts = string.match(password, "(%w+)_(%w+)_(%d+)"); + if hash ~= nil and room ~= nil and ts ~= nil then + log("debug", "Hash: '%s' room: '%s', ts: '%s'", hash, room, ts); + local toHash = room .. ts .. appId .. appSecret; + log("debug", "to be hashed: '%s'", toHash); + local hash = hashes.sha256(toHash, true); + log("debug", "hash: '%s'", hash); + return hash; + else + log("error", "Invalid password format: '%s'", password); + return nil; + end +end + +local function extract_hash(password) + local hash, room, ts = string.match(password, "(%w+)_(%w+)_(%d+)"); + return hash; +end + +local function extract_ts(password) + local hash, room, ts = string.match(password, "(%w+)_(%w+)_(%d+)"); + return ts; +end + +local function get_utc_timestamp() + return os.time(os.date("!*t")) * 1000; +end + +local function verify_timestamp(ts, tokenLifetime) + return get_utc_timestamp() - ts <= tokenLifetime; +end + +local function verify_password_impl(password, appId, appSecret, tokenLifetime) + + if password == nil then + return nil, "password is missing"; + end + + if tokenLifetime == nil then + tokenLifetime = 24 * 60 * 60 * 1000; + end + + local ts = extract_ts(password); + if ts == nil then + return nil, "timestamp not found in the password"; + end + local os_ts = get_utc_timestamp(); + log("debug", "System TS: '%s' user TS: %s", tostring(os_ts), tostring(ts)); + local isValid = verify_timestamp(ts, tokenLifetime); + if not isValid then + return nil, "token expired"; + end + + local realHash = calc_hash(password, appId, appSecret); + local givenhash = extract_hash(password); + log("debug", "Compare '%s' to '%s'", tostring(realHash), tostring(givenhash)); + if realHash == givenhash then + return true; + else + return nil, "invalid hash"; + end +end + +function _M.verify_password(password, appId, appSecret, tokenLifetime) + return verify_password_impl(password, appId, appSecret, tokenLifetime); +end + +return _M;