Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

util.lib.lua 16KB

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