您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

util.lib.lua 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. -- Token authentication
  2. -- Copyright (C) 2015 Atlassian
  3. local basexx = require "basexx";
  4. local have_async, async = pcall(require, "util.async");
  5. local hex = require "util.hex";
  6. local jwt = require "luajwtjitsi";
  7. local http = require "net.http";
  8. local jid = require "util.jid";
  9. local json = require "cjson";
  10. local path = require "util.paths";
  11. local sha256 = require "util.hashes".sha256;
  12. local timer = require "util.timer";
  13. local http_timeout = 30;
  14. local http_headers = {
  15. ["User-Agent"] = "Prosody ("..prosody.version.."; "..prosody.platform..")"
  16. };
  17. -- TODO: Figure out a less arbitrary default cache size.
  18. local cacheSize = module:get_option_number("jwt_pubkey_cache_size", 128);
  19. local cache = require"util.cache".new(cacheSize);
  20. local Util = {}
  21. Util.__index = Util
  22. --- Constructs util class for token verifications.
  23. -- Constructor that uses the passed module to extract all the
  24. -- needed configurations.
  25. -- If confuguration is missing returns nil
  26. -- @param module the module in which options to check for configs.
  27. -- @return the new instance or nil
  28. function Util.new(module)
  29. local self = setmetatable({}, Util)
  30. self.appId = module:get_option_string("app_id");
  31. self.appSecret = module:get_option_string("app_secret");
  32. self.asapKeyServer = module:get_option_string("asap_key_server");
  33. self.allowEmptyToken = module:get_option_boolean("allow_empty_token");
  34. --[[
  35. Multidomain can be supported in some deployments. In these deployments
  36. there is a virtual conference muc, which address contains the subdomain
  37. to use. Those deployments are accessible
  38. by URL https://domain/subdomain.
  39. Then the address of the room will be:
  40. roomName@conference.subdomain.domain. This is like a virtual address
  41. where there is only one muc configured by default with address:
  42. conference.domain and the actual presentation of the room in that muc
  43. component is [subdomain]roomName@conference.domain.
  44. These setups relay on configuration 'muc_domain_base' which holds
  45. the main domain and we use it to substract subdomains from the
  46. virtual addresses.
  47. The following confgurations are for multidomain setups and domain name
  48. verification:
  49. --]]
  50. -- optional parameter for custom muc component prefix,
  51. -- defaults to "conference"
  52. self.muc_domain_prefix = module:get_option_string(
  53. "muc_mapper_domain_prefix", "conference");
  54. -- domain base, which is the main domain used in the deployment,
  55. -- the main VirtualHost for the deployment
  56. self.muc_domain_base = module:get_option_string("muc_mapper_domain_base");
  57. -- The "real" MUC domain that we are proxying to
  58. if self.muc_domain_base then
  59. self.muc_domain = module:get_option_string(
  60. "muc_mapper_domain",
  61. self.muc_domain_prefix.."."..self.muc_domain_base);
  62. end
  63. -- whether domain name verification is enabled, by default it is disabled
  64. self.enableDomainVerification = module:get_option_boolean(
  65. "enable_domain_verification", false);
  66. if self.allowEmptyToken == true then
  67. module:log("warn", "WARNING - empty tokens allowed");
  68. end
  69. if self.appId == nil then
  70. module:log("error", "'app_id' must not be empty");
  71. return nil;
  72. end
  73. if self.appSecret == nil and self.asapKeyServer == nil then
  74. module:log("error", "'app_secret' or 'asap_key_server' must be specified");
  75. return nil;
  76. end
  77. if self.asapKeyServer and not have_async then
  78. module:log("error", "requires a version of Prosody with util.async");
  79. return nil;
  80. end
  81. return self
  82. end
  83. --- Returns the public key by keyID
  84. -- @param keyId the key ID to request
  85. -- @return the public key (the content of requested resource) or nil
  86. function Util:get_public_key(keyId)
  87. local content = cache:get(keyId);
  88. if content == nil then
  89. -- If the key is not found in the cache.
  90. module:log("debug", "Cache miss for key: "..keyId);
  91. local code;
  92. local wait, done = async.waiter();
  93. local function cb(content_, code_, response_, request_)
  94. content, code = content_, code_;
  95. if code == 200 or code == 204 then
  96. cache:set(keyId, content);
  97. end
  98. done();
  99. end
  100. local keyurl = path.join(self.asapKeyServer, hex.to(sha256(keyId))..'.pem');
  101. module:log("debug", "Fetching public key from: "..keyurl);
  102. -- We hash the key ID to work around some legacy behavior and make
  103. -- deployment easier. It also helps prevent directory
  104. -- traversal attacks (although path cleaning could have done this too).
  105. local request = http.request(keyurl, {
  106. headers = http_headers or {},
  107. method = "GET"
  108. }, cb);
  109. -- TODO: Is the done() call racey? Can we cancel this if the request
  110. -- succeedes?
  111. local function cancel()
  112. -- TODO: This check is racey. Not likely to be a problem, but we should
  113. -- still stick a mutex on content / code at some point.
  114. if code == nil then
  115. http.destroy_request(request);
  116. done();
  117. end
  118. end
  119. timer.add_task(http_timeout, cancel);
  120. wait();
  121. if code == 200 or code == 204 then
  122. return content;
  123. end
  124. else
  125. -- If the key is in the cache, use it.
  126. module:log("debug", "Cache hit for key: "..keyId);
  127. return content;
  128. end
  129. return nil;
  130. end
  131. --- Verifies token
  132. -- @param token the token to verify
  133. -- @return nil and error or the extracted claims from the token
  134. function Util:verify_token(token)
  135. local claims, err = jwt.decode(token, self.appSecret, true);
  136. if claims == nil then
  137. return nil, err;
  138. end
  139. local alg = claims["alg"];
  140. if alg ~= nil and (alg == "none" or alg == "") then
  141. return nil, "'alg' claim must not be empty";
  142. end
  143. local issClaim = claims["iss"];
  144. if issClaim == nil then
  145. return nil, "'iss' claim is missing";
  146. end
  147. if issClaim ~= self.appId then
  148. return nil, "Invalid application ID('iss' claim)";
  149. end
  150. local roomClaim = claims["room"];
  151. if roomClaim == nil then
  152. return nil, "'room' claim is missing";
  153. end
  154. local audClaim = claims["aud"];
  155. if audClaim == nil then
  156. return nil, "'aud' claim is missing";
  157. end
  158. return claims;
  159. end
  160. --- Verifies token and process needed values to be stored in the session.
  161. -- Token is obtained from session.auth_token.
  162. -- Stores in session the following values:
  163. -- session.jitsi_meet_room - the room name value from the token
  164. -- session.jitsi_meet_domain - the domain name value from the token
  165. -- @param session the current session
  166. -- @return false and error
  167. function Util:process_and_verify_token(session)
  168. if session.auth_token == nil then
  169. if self.allowEmptyToken then
  170. return true;
  171. else
  172. return false, "not-allowed", "token required";
  173. end
  174. end
  175. local pubKey;
  176. if self.asapKeyServer and session.auth_token ~= nil then
  177. local dotFirst = session.auth_token:find("%.");
  178. if not dotFirst then return nil, "Invalid token" end
  179. local header = json.decode(basexx.from_url64(session.auth_token:sub(1,dotFirst-1)));
  180. local kid = header["kid"];
  181. if kid == nil then
  182. return false, "not-allowed", "'kid' claim is missing";
  183. end
  184. pubKey = self:get_public_key(kid);
  185. if pubKey == nil then
  186. return false, "not-allowed", "could not obtain public key";
  187. end
  188. end
  189. -- now verify the whole token
  190. local claims, msg;
  191. if self.asapKeyServer then
  192. claims, msg = self:verify_token(session.auth_token);
  193. else
  194. claims, msg = self:verify_token(session.auth_token);
  195. end
  196. if claims ~= nil then
  197. -- Binds room name to the session which is later checked on MUC join
  198. session.jitsi_meet_room = claims["room"];
  199. -- Binds domain name to the session
  200. session.jitsi_meet_domain = claims["aud"];
  201. return true;
  202. else
  203. return false, "not-allowed", msg;
  204. end
  205. end
  206. --- Verifies room name and domain if necesarry.
  207. -- Checks configs and if necessary checks the room name extracted from
  208. -- room_address against the one saved in the session when token was verified.
  209. -- Also verifies domain name from token against the domain in the room_address,
  210. -- if enableDomainVerification is enabled.
  211. -- @param session the current session
  212. -- @param room_address the whole room address as received
  213. -- @return returns true in case room was verified or there is no need to verify
  214. -- it and returns false in case verification was processed
  215. -- and was not successful
  216. function Util:verify_room(session, room_address)
  217. if self.allowEmptyToken and session.auth_token == nil then
  218. module:log(
  219. "debug",
  220. "Skipped room token verification - empty tokens are allowed");
  221. return true;
  222. end
  223. -- extract room name using all chars, except the not allowed ones
  224. local room,_,_ = jid.split(room_address);
  225. if room == nil then
  226. log("error",
  227. "Unable to get name of the MUC room ? to: %s", room_address);
  228. return true;
  229. end
  230. local auth_room = session.jitsi_meet_room;
  231. if not self.enableDomainVerification then
  232. -- if auth_room is missing, this means user is anonymous (no token for
  233. -- its domain) we let it through, jicofo is verifying creation domain
  234. if auth_room and room ~= string.lower(auth_room) then
  235. return false;
  236. end
  237. return true;
  238. end
  239. local room_address_to_verify = jid.bare(room_address);
  240. -- parses bare room address, for multidomain expected format is:
  241. -- [subdomain]roomName@conference.domain
  242. local target_subdomain, target_room
  243. = room_address_to_verify:match("^%[([^%]]+)%](.+)$");
  244. local auth_domain = session.jitsi_meet_domain;
  245. if target_subdomain then
  246. -- from this point we depend on muc_domain_base,
  247. -- deny access if option is missing
  248. if not self.muc_domain_base then
  249. module:log("warn", "No 'muc_domain_base' option set, denying access!");
  250. return false;
  251. end
  252. return room_address_to_verify == jid.join(
  253. "["..auth_domain.."]"..string.lower(auth_room), self.muc_domain);
  254. else
  255. -- we do not have a domain part (multidomain is not enabled)
  256. -- verify with info from the token
  257. return room_address_to_verify == jid.join(
  258. string.lower(auth_room), self.muc_domain_prefix.."."..auth_domain);
  259. end
  260. end
  261. return Util;