Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

util.lib.lua 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  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_safe = require "cjson.safe";
  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 Util = {}
  20. Util.__index = Util
  21. --- Constructs util class for token verifications.
  22. -- Constructor that uses the passed module to extract all the
  23. -- needed configurations.
  24. -- If confuguration is missing returns nil
  25. -- @param module the module in which options to check for configs.
  26. -- @return the new instance or nil
  27. function Util.new(module)
  28. local self = setmetatable({}, Util)
  29. self.appId = module:get_option_string("app_id");
  30. self.appSecret = module:get_option_string("app_secret");
  31. self.asapKeyServer = module:get_option_string("asap_key_server");
  32. self.allowEmptyToken = module:get_option_boolean("allow_empty_token");
  33. self.cache = require"util.cache".new(cacheSize);
  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. --array of accepted issuers: by default only includes our appId
  78. self.acceptedIssuers = module:get_option_array('asap_accepted_issuers',{self.appId})
  79. --array of accepted audiences: by default only includes our appId
  80. self.acceptedAudiences = module:get_option_array('asap_accepted_audiences',{'*'})
  81. if self.asapKeyServer and not have_async then
  82. module:log("error", "requires a version of Prosody with util.async");
  83. return nil;
  84. end
  85. return self
  86. end
  87. function Util:set_asap_key_server(asapKeyServer)
  88. self.asapKeyServer = asapKeyServer
  89. end
  90. --- Returns the public key by keyID
  91. -- @param keyId the key ID to request
  92. -- @return the public key (the content of requested resource) or nil
  93. function Util:get_public_key(keyId)
  94. local content = self.cache:get(keyId);
  95. if content == nil then
  96. -- If the key is not found in the cache.
  97. module:log("debug", "Cache miss for key: "..keyId);
  98. local code;
  99. local wait, done = async.waiter();
  100. local function cb(content_, code_, response_, request_)
  101. content, code = content_, code_;
  102. if code == 200 or code == 204 then
  103. self.cache:set(keyId, content);
  104. else
  105. module:log("warn", "Error on public key request: Code %s, Content %s",
  106. code_, content_);
  107. end
  108. done();
  109. end
  110. local keyurl = path.join(self.asapKeyServer, hex.to(sha256(keyId))..'.pem');
  111. module:log("debug", "Fetching public key from: "..keyurl);
  112. -- We hash the key ID to work around some legacy behavior and make
  113. -- deployment easier. It also helps prevent directory
  114. -- traversal attacks (although path cleaning could have done this too).
  115. local request = http.request(keyurl, {
  116. headers = http_headers or {},
  117. method = "GET"
  118. }, cb);
  119. -- TODO: Is the done() call racey? Can we cancel this if the request
  120. -- succeedes?
  121. local function cancel()
  122. -- TODO: This check is racey. Not likely to be a problem, but we should
  123. -- still stick a mutex on content / code at some point.
  124. if code == nil then
  125. -- no longer present in prosody 0.11, so check before calling
  126. if http.destroy_request ~= nil then
  127. http.destroy_request(request);
  128. end
  129. done();
  130. end
  131. end
  132. timer.add_task(http_timeout, cancel);
  133. wait();
  134. if code == 200 or code == 204 then
  135. return content;
  136. end
  137. else
  138. -- If the key is in the cache, use it.
  139. module:log("debug", "Cache hit for key: "..keyId);
  140. return content;
  141. end
  142. return nil;
  143. end
  144. --- Verifies issuer part of token
  145. -- @param 'iss' claim from the token to verify
  146. -- @param 'acceptedIssuers' list of issuers to check
  147. -- @return nil and error string or true for accepted claim
  148. function Util:verify_issuer(issClaim, acceptedIssuers)
  149. for i, iss in ipairs(acceptedIssuers) do
  150. if issClaim == iss then
  151. --claim matches an accepted issuer so return success
  152. return true;
  153. end
  154. end
  155. --if issClaim not found in acceptedIssuers, fail claim
  156. return nil, "Invalid issuer ('iss' claim)";
  157. end
  158. --- Verifies audience part of token
  159. -- @param 'aud' claim from the token to verify
  160. -- @return nil and error string or true for accepted claim
  161. function Util:verify_audience(audClaim)
  162. for i, aud in ipairs(self.acceptedAudiences) do
  163. if aud == '*' then
  164. --* indicates to accept any audience in the claims so return success
  165. return true;
  166. end
  167. if audClaim == aud then
  168. --claim matches an accepted audience so return success
  169. return true;
  170. end
  171. end
  172. --if issClaim not found in acceptedIssuers, fail claim
  173. return nil, "Invalid audience ('aud' claim)";
  174. end
  175. --- Verifies token
  176. -- @param token the token to verify
  177. -- @param secret the secret to use to verify token
  178. -- @param acceptedIssuers the list of accepted issuers to check
  179. -- @return nil and error or the extracted claims from the token
  180. function Util:verify_token(token, secret, acceptedIssuers)
  181. local claims, err = jwt.decode(token, secret, true);
  182. if claims == nil then
  183. return nil, err;
  184. end
  185. local alg = claims["alg"];
  186. if alg ~= nil and (alg == "none" or alg == "") then
  187. return nil, "'alg' claim must not be empty";
  188. end
  189. local issClaim = claims["iss"];
  190. if issClaim == nil then
  191. return nil, "'iss' claim is missing";
  192. end
  193. --check the issuer against the accepted list
  194. local issCheck, issCheckErr = self:verify_issuer(issClaim, acceptedIssuers);
  195. if issCheck == nil then
  196. return nil, issCheckErr;
  197. end
  198. local roomClaim = claims["room"];
  199. if roomClaim == nil then
  200. return nil, "'room' claim is missing";
  201. end
  202. local audClaim = claims["aud"];
  203. if audClaim == nil then
  204. return nil, "'aud' claim is missing";
  205. end
  206. --check the audience against the accepted list
  207. local audCheck, audCheckErr = self:verify_audience(audClaim);
  208. if audCheck == nil then
  209. return nil, audCheckErr;
  210. end
  211. return claims;
  212. end
  213. --- Verifies token and process needed values to be stored in the session.
  214. -- Token is obtained from session.auth_token.
  215. -- Stores in session the following values:
  216. -- session.jitsi_meet_room - the room name value from the token
  217. -- session.jitsi_meet_domain - the domain name value from the token
  218. -- session.jitsi_meet_context_user - the user details from the token
  219. -- session.jitsi_meet_context_group - the group value from the token
  220. -- session.jitsi_meet_context_features - the features value from the token
  221. -- @param session the current session
  222. -- @param acceptedIssuers optional list of accepted issuers to check
  223. -- @return false and error
  224. function Util:process_and_verify_token(session, acceptedIssuers)
  225. if not acceptedIssuers then
  226. acceptedIssuers = self.acceptedIssuers;
  227. end
  228. if session.auth_token == nil then
  229. if self.allowEmptyToken then
  230. return true;
  231. else
  232. return false, "not-allowed", "token required";
  233. end
  234. end
  235. local pubKey;
  236. if self.asapKeyServer and session.auth_token ~= nil then
  237. local dotFirst = session.auth_token:find("%.");
  238. if not dotFirst then return nil, "Invalid token" end
  239. local header, err = json_safe.decode(basexx.from_url64(session.auth_token:sub(1,dotFirst-1)));
  240. if err then
  241. return false, "not-allowed", "bad token format";
  242. end
  243. local kid = header["kid"];
  244. if kid == nil then
  245. return false, "not-allowed", "'kid' claim is missing";
  246. end
  247. pubKey = self:get_public_key(kid);
  248. if pubKey == nil then
  249. return false, "not-allowed", "could not obtain public key";
  250. end
  251. end
  252. -- now verify the whole token
  253. local claims, msg;
  254. if self.asapKeyServer then
  255. claims, msg = self:verify_token(session.auth_token, pubKey, acceptedIssuers);
  256. else
  257. claims, msg = self:verify_token(session.auth_token, self.appSecret, acceptedIssuers);
  258. end
  259. if claims ~= nil then
  260. -- Binds room name to the session which is later checked on MUC join
  261. session.jitsi_meet_room = claims["room"];
  262. -- Binds domain name to the session
  263. session.jitsi_meet_domain = claims["sub"];
  264. -- Binds the user details to the session if available
  265. if claims["context"] ~= nil then
  266. if claims["context"]["user"] ~= nil then
  267. session.jitsi_meet_context_user = claims["context"]["user"];
  268. end
  269. if claims["context"]["group"] ~= nil then
  270. -- Binds any group details to the session
  271. session.jitsi_meet_context_group = claims["context"]["group"];
  272. end
  273. if claims["context"]["features"] ~= nil then
  274. -- Binds any features details to the session
  275. session.jitsi_meet_context_features = claims["context"]["features"];
  276. end
  277. end
  278. return true;
  279. else
  280. return false, "not-allowed", msg;
  281. end
  282. end
  283. --- Verifies room name and domain if necesarry.
  284. -- Checks configs and if necessary checks the room name extracted from
  285. -- room_address against the one saved in the session when token was verified.
  286. -- Also verifies domain name from token against the domain in the room_address,
  287. -- if enableDomainVerification is enabled.
  288. -- @param session the current session
  289. -- @param room_address the whole room address as received
  290. -- @return returns true in case room was verified or there is no need to verify
  291. -- it and returns false in case verification was processed
  292. -- and was not successful
  293. function Util:verify_room(session, room_address)
  294. if self.allowEmptyToken and session.auth_token == nil then
  295. module:log(
  296. "debug",
  297. "Skipped room token verification - empty tokens are allowed");
  298. return true;
  299. end
  300. -- extract room name using all chars, except the not allowed ones
  301. local room,_,_ = jid.split(room_address);
  302. if room == nil then
  303. log("error",
  304. "Unable to get name of the MUC room ? to: %s", room_address);
  305. return true;
  306. end
  307. local auth_room = session.jitsi_meet_room;
  308. if not self.enableDomainVerification then
  309. -- if auth_room is missing, this means user is anonymous (no token for
  310. -- its domain) we let it through, jicofo is verifying creation domain
  311. if auth_room and room ~= string.lower(auth_room) and auth_room ~= '*' then
  312. return false;
  313. end
  314. return true;
  315. end
  316. local room_address_to_verify = jid.bare(room_address);
  317. local room_node = jid.node(room_address);
  318. -- parses bare room address, for multidomain expected format is:
  319. -- [subdomain]roomName@conference.domain
  320. local target_subdomain, target_room = room_node:match("^%[([^%]]+)%](.+)$");
  321. -- if we have '*' as room name in token, this means all rooms are allowed
  322. -- so we will use the actual name of the room when constructing strings
  323. -- to verify subdomains and domains to simplify checks
  324. local room_to_check;
  325. if auth_room == '*' then
  326. -- authorized for accessing any room assign to room_to_check the actual
  327. -- room name
  328. if target_room ~= nil then
  329. -- we are in multidomain mode and we were able to extract room name
  330. room_to_check = target_room;
  331. else
  332. -- no target_room, room_address_to_verify does not contain subdomain
  333. -- so we get just the node which is the room name
  334. room_to_check = room_node;
  335. end
  336. else
  337. -- no wildcard, so check room against authorized room in token
  338. room_to_check = auth_room;
  339. end
  340. local auth_domain = session.jitsi_meet_domain;
  341. local subdomain_to_check;
  342. if target_subdomain then
  343. if auth_domain == '*' then
  344. -- check for wildcard in JWT claim, allow access if found
  345. subdomain_to_check = target_subdomain;
  346. else
  347. -- no wildcard in JWT claim, so check subdomain against sub in token
  348. subdomain_to_check = auth_domain;
  349. end
  350. -- from this point we depend on muc_domain_base,
  351. -- deny access if option is missing
  352. if not self.muc_domain_base then
  353. module:log("warn", "No 'muc_domain_base' option set, denying access!");
  354. return false;
  355. end
  356. return room_address_to_verify == jid.join(
  357. "["..string.lower(subdomain_to_check).."]"..string.lower(room_to_check), self.muc_domain);
  358. else
  359. if auth_domain == '*' then
  360. -- check for wildcard in JWT claim, allow access if found
  361. subdomain_to_check = self.muc_domain;
  362. else
  363. -- no wildcard in JWT claim, so check subdomain against sub in token
  364. subdomain_to_check = self.muc_domain_prefix.."."..auth_domain;
  365. end
  366. -- we do not have a domain part (multidomain is not enabled)
  367. -- verify with info from the token
  368. return room_address_to_verify == jid.join(
  369. string.lower(room_to_check), string.lower(subdomain_to_check));
  370. end
  371. end
  372. return Util;