diff --git a/prosody-plugins/mod_auth_token.lua b/prosody-plugins/mod_auth_token.lua index 28300aa4b..c7a187fbf 100644 --- a/prosody-plugins/mod_auth_token.lua +++ b/prosody-plugins/mod_auth_token.lua @@ -1,10 +1,16 @@ -- Token authentication -- Copyright (C) 2015 Atlassian +local basexx = require 'basexx'; +local have_async, async = pcall(require, "util.async"); +local formdecode = require "util.http".formdecode; local generate_uuid = require "util.uuid".generate; +local http = require "net.http"; +local json = require 'cjson' +json.encode_empty_table('array') local new_sasl = require "util.sasl".new; local sasl = require "util.sasl"; -local formdecode = require "util.http".formdecode; +local timer = require "util.timer"; local token_util = module:require "token/util"; -- define auth provider @@ -14,9 +20,14 @@ local host = module.host; local appId = module:get_option_string("app_id"); local appSecret = module:get_option_string("app_secret"); +local asapKeyServer = module:get_option_string("asap_key_server"); local allowEmptyToken = module:get_option_boolean("allow_empty_token"); local disableRoomNameConstraints = module:get_option_boolean("disable_room_name_constraints"); +-- TODO: Figure out a less arbitrary default cache size. +local cacheSize = module:get_option_number("jwt_pubkey_cache_size", 128); +local cache = require"util.cache".new(cacheSize); + if allowEmptyToken == true then module:log("warn", "WARNING - empty tokens allowed"); end @@ -26,8 +37,13 @@ if appId == nil then return; end -if appSecret == nil then - module:log("error", "'app_secret' must not be empty"); +if appSecret == nil and asapKeyServer == nil then + module:log("error", "'app_secret' or 'asap_key_server' must be specified"); + return; +end + +if asapKeyServer and not have_async then + module:log("error", "requires a version of Prosody with util.async"); return; end @@ -64,6 +80,52 @@ function provider.delete_user(username) return nil; end +local http_timeout = 30; +local http_headers = { + ["User-Agent"] = "Prosody ("..prosody.version.."; "..prosody.platform..")" +}; + +function get_public_key(keyId) + local content = cache:get(keyId); + if content == nil then + -- If the key is not found in the cache. + module:log("debug", "Cache miss for key: "..keyId); + local code; + local wait, done = async.waiter(); + local function cb(content_, code_, response_, request_) + content, code = content_, code_; + done(); + end + module:log("debug", "Fetching public key from: "..asapKeyServer..keyId); + local request = http.request(asapKeyServer..keyId, { + headers = http_headers or {}, + method = "GET" + }, cb); + -- TODO: Is the done() call racey? Can we cancel this if the request + -- succeedes? + local function cancel() + -- TODO: This check is racey. Not likely to be a problem, but we should + -- still stick a mutex on content / code at some point. + if code == nil then + http.destroy_request(request); + done(); + end + end + timer.add_task(http_timeout, cancel); + wait(); + + if code == 200 or code == 204 then + return content; + end + else + -- If the key is in the cache, use it. + module:log("debug", "Cache hit for key: "..keyId); + return content; + end + + return nil; +end + function provider.get_sasl_handler(session) -- JWT token extracted from BOSH URL local token = session.auth_token; @@ -71,29 +133,39 @@ function provider.get_sasl_handler(session) local function get_username_from_token(self, message) if token == nil then - if allowEmptyToken == true then + if allowEmptyToken then return true; else return false, "not-allowed", "token required"; end end - -- here we check if 'room' claim exists - local room, roomErr = token_util.get_room_name(token, appSecret); - if room == nil and disableRoomNameConstraints ~= true then - if roomErr == nil then - roomErr = "'room' claim is missing"; - end - return false, "not-allowed", roomErr; + local pubKey; + if asapKeyServer and session.auth_token ~= nil then + local dotFirst = session.auth_token:find("%.") + if not dotFirst then return nil, "Invalid token" end + local header = json.decode(basexx.from_url64(session.auth_token:sub(1,dotFirst-1))) + local kid = header["kid"] + if kid == nil then + return false, "not-allowed", "'kid' claim is missing"; + end + pubKey = get_public_key(kid); + if pubKey == nil then + return false, "not-allowed", "could not obtain public key"; + end end -- now verify the whole token - local result, msg - = token_util.verify_token(token, appId, appSecret, room, disableRoomNameConstraints); - if result == true then + local claims, msg; + if asapKeyServer then + claims, msg = token_util.verify_token(token, appId, pubKey, disableRoomNameConstraints); + else + claims, msg = token_util.verify_token(token, appId, appSecret, disableRoomNameConstraints); + end + if claims ~= nil then -- Binds room name to the session which is later checked on MUC join - session.jitsi_meet_room = room; - return true + session.jitsi_meet_room = claims["room"]; + return true; else return false, "not-allowed", msg end diff --git a/prosody-plugins/token/util.lib.lua b/prosody-plugins/token/util.lib.lua index 641ce1d78..7ef885a3a 100644 --- a/prosody-plugins/token/util.lib.lua +++ b/prosody-plugins/token/util.lib.lua @@ -5,16 +5,7 @@ local jwt = require "luajwtjitsi"; local _M = {}; -local function _get_room_name(token, appSecret) - local claims, err = jwt.decode(token, appSecret); - if claims ~= nil then - return claims["room"]; - else - return nil, err; - end -end - -local function _verify_token(token, appId, appSecret, roomName, disableRoomNameConstraints) +local function _verify_token(token, appId, appSecret, disableRoomNameConstraints) local claims, err = jwt.decode(token, appSecret, true); if claims == nil then @@ -38,19 +29,12 @@ local function _verify_token(token, appId, appSecret, roomName, disableRoomNameC if roomClaim == nil and disableRoomNameConstraints ~= true then return nil, "'room' claim is missing"; end - if roomName ~= nil and roomName ~= roomClaim and disableRoomNameConstraints ~= true then - return nil, "Invalid room name('room' claim)"; - end - return true; + return claims; end -function _M.verify_token(token, appId, appSecret, roomName, disableRoomNameConstraints) - return _verify_token(token, appId, appSecret, roomName, disableRoomNameConstraints); -end - -function _M.get_room_name(token, appSecret) - return _get_room_name(token, appSecret); +function _M.verify_token(token, appId, appSecret, disableRoomNameConstraints) + return _verify_token(token, appId, appSecret, disableRoomNameConstraints); end return _M;