You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

util.lib.lua 17KB

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