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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  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 session.public_key then
  263. module:log("debug","Public key was found on the session");
  264. pubKey = session.public_key;
  265. elseif self.asapKeyServer and session.auth_token ~= nil then
  266. local dotFirst = session.auth_token:find("%.");
  267. if not dotFirst then return nil, "Invalid token" end
  268. local header, err = json_safe.decode(basexx.from_url64(session.auth_token:sub(1,dotFirst-1)));
  269. if err then
  270. return false, "not-allowed", "bad token format";
  271. end
  272. local kid = header["kid"];
  273. if kid == nil then
  274. return false, "not-allowed", "'kid' claim is missing";
  275. end
  276. pubKey = self:get_public_key(kid);
  277. if pubKey == nil then
  278. return false, "not-allowed", "could not obtain public key";
  279. end
  280. end
  281. -- now verify the whole token
  282. local claims, msg;
  283. if self.asapKeyServer then
  284. claims, msg = self:verify_token(session.auth_token, pubKey, acceptedIssuers);
  285. else
  286. claims, msg = self:verify_token(session.auth_token, self.appSecret, acceptedIssuers);
  287. end
  288. if claims ~= nil then
  289. -- Binds room name to the session which is later checked on MUC join
  290. session.jitsi_meet_room = claims["room"];
  291. -- Binds domain name to the session
  292. session.jitsi_meet_domain = claims["sub"];
  293. -- Binds the user details to the session if available
  294. if claims["context"] ~= nil then
  295. if claims["context"]["user"] ~= nil then
  296. session.jitsi_meet_context_user = claims["context"]["user"];
  297. end
  298. if claims["context"]["group"] ~= nil then
  299. -- Binds any group details to the session
  300. session.jitsi_meet_context_group = claims["context"]["group"];
  301. end
  302. if claims["context"]["features"] ~= nil then
  303. -- Binds any features details to the session
  304. session.jitsi_meet_context_features = claims["context"]["features"];
  305. end
  306. end
  307. return true;
  308. else
  309. return false, "not-allowed", msg;
  310. end
  311. end
  312. --- Verifies room name and domain if necesarry.
  313. -- Checks configs and if necessary checks the room name extracted from
  314. -- room_address against the one saved in the session when token was verified.
  315. -- Also verifies domain name from token against the domain in the room_address,
  316. -- if enableDomainVerification is enabled.
  317. -- @param session the current session
  318. -- @param room_address the whole room address as received
  319. -- @return returns true in case room was verified or there is no need to verify
  320. -- it and returns false in case verification was processed
  321. -- and was not successful
  322. function Util:verify_room(session, room_address)
  323. if self.allowEmptyToken and session.auth_token == nil then
  324. module:log(
  325. "debug",
  326. "Skipped room token verification - empty tokens are allowed");
  327. return true;
  328. end
  329. -- extract room name using all chars, except the not allowed ones
  330. local room,_,_ = jid.split(room_address);
  331. if room == nil then
  332. log("error",
  333. "Unable to get name of the MUC room ? to: %s", room_address);
  334. return true;
  335. end
  336. local auth_room = session.jitsi_meet_room;
  337. if not self.enableDomainVerification then
  338. -- if auth_room is missing, this means user is anonymous (no token for
  339. -- its domain) we let it through, jicofo is verifying creation domain
  340. if auth_room and room ~= string.lower(auth_room) and auth_room ~= '*' then
  341. return false;
  342. end
  343. return true;
  344. end
  345. local room_address_to_verify = jid.bare(room_address);
  346. local room_node = jid.node(room_address);
  347. -- parses bare room address, for multidomain expected format is:
  348. -- [subdomain]roomName@conference.domain
  349. local target_subdomain, target_room = room_node:match("^%[([^%]]+)%](.+)$");
  350. -- if we have '*' as room name in token, this means all rooms are allowed
  351. -- so we will use the actual name of the room when constructing strings
  352. -- to verify subdomains and domains to simplify checks
  353. local room_to_check;
  354. if auth_room == '*' then
  355. -- authorized for accessing any room assign to room_to_check the actual
  356. -- room name
  357. if target_room ~= nil then
  358. -- we are in multidomain mode and we were able to extract room name
  359. room_to_check = target_room;
  360. else
  361. -- no target_room, room_address_to_verify does not contain subdomain
  362. -- so we get just the node which is the room name
  363. room_to_check = room_node;
  364. end
  365. else
  366. -- no wildcard, so check room against authorized room in token
  367. room_to_check = auth_room;
  368. end
  369. local auth_domain = session.jitsi_meet_domain;
  370. local subdomain_to_check;
  371. if target_subdomain then
  372. if auth_domain == '*' then
  373. -- check for wildcard in JWT claim, allow access if found
  374. subdomain_to_check = target_subdomain;
  375. else
  376. -- no wildcard in JWT claim, so check subdomain against sub in token
  377. subdomain_to_check = auth_domain;
  378. end
  379. -- from this point we depend on muc_domain_base,
  380. -- deny access if option is missing
  381. if not self.muc_domain_base then
  382. module:log("warn", "No 'muc_domain_base' option set, denying access!");
  383. return false;
  384. end
  385. return room_address_to_verify == jid.join(
  386. "["..string.lower(subdomain_to_check).."]"..string.lower(room_to_check), self.muc_domain);
  387. else
  388. if auth_domain == '*' then
  389. -- check for wildcard in JWT claim, allow access if found
  390. subdomain_to_check = self.muc_domain;
  391. else
  392. -- no wildcard in JWT claim, so check subdomain against sub in token
  393. subdomain_to_check = self.muc_domain_prefix.."."..auth_domain;
  394. end
  395. -- we do not have a domain part (multidomain is not enabled)
  396. -- verify with info from the token
  397. return room_address_to_verify == jid.join(
  398. string.lower(room_to_check), string.lower(subdomain_to_check));
  399. end
  400. end
  401. return Util;