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 15KB

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