Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

util.lib.lua 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  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 jid = require "util.jid";
  8. local json_safe = require "cjson.safe";
  9. local path = require "util.paths";
  10. local sha256 = require "util.hashes".sha256;
  11. local main_util = module:require "util";
  12. local http_get_with_retry = main_util.http_get_with_retry;
  13. local extract_subdomain = main_util.extract_subdomain;
  14. local nr_retries = 3;
  15. -- TODO: Figure out a less arbitrary default cache size.
  16. local cacheSize = module:get_option_number("jwt_pubkey_cache_size", 128);
  17. local Util = {}
  18. Util.__index = Util
  19. --- Constructs util class for token verifications.
  20. -- Constructor that uses the passed module to extract all the
  21. -- needed configurations.
  22. -- If confuguration is missing returns nil
  23. -- @param module the module in which options to check for configs.
  24. -- @return the new instance or nil
  25. function Util.new(module)
  26. local self = setmetatable({}, Util)
  27. self.appId = module:get_option_string("app_id");
  28. self.appSecret = module:get_option_string("app_secret");
  29. self.asapKeyServer = module:get_option_string("asap_key_server");
  30. self.allowEmptyToken = module:get_option_boolean("allow_empty_token");
  31. self.cache = require"util.cache".new(cacheSize);
  32. --[[
  33. Multidomain can be supported in some deployments. In these deployments
  34. there is a virtual conference muc, which address contains the subdomain
  35. to use. Those deployments are accessible
  36. by URL https://domain/subdomain.
  37. Then the address of the room will be:
  38. roomName@conference.subdomain.domain. This is like a virtual address
  39. where there is only one muc configured by default with address:
  40. conference.domain and the actual presentation of the room in that muc
  41. component is [subdomain]roomName@conference.domain.
  42. These setups relay on configuration 'muc_domain_base' which holds
  43. the main domain and we use it to substract subdomains from the
  44. virtual addresses.
  45. The following confgurations are for multidomain setups and domain name
  46. verification:
  47. --]]
  48. -- optional parameter for custom muc component prefix,
  49. -- defaults to "conference"
  50. self.muc_domain_prefix = module:get_option_string(
  51. "muc_mapper_domain_prefix", "conference");
  52. -- domain base, which is the main domain used in the deployment,
  53. -- the main VirtualHost for the deployment
  54. self.muc_domain_base = module:get_option_string("muc_mapper_domain_base");
  55. -- The "real" MUC domain that we are proxying to
  56. if self.muc_domain_base then
  57. self.muc_domain = module:get_option_string(
  58. "muc_mapper_domain",
  59. self.muc_domain_prefix.."."..self.muc_domain_base);
  60. end
  61. -- whether domain name verification is enabled, by default it is disabled
  62. self.enableDomainVerification = module:get_option_boolean(
  63. "enable_domain_verification", false);
  64. if self.allowEmptyToken == true then
  65. module:log("warn", "WARNING - empty tokens allowed");
  66. end
  67. if self.appId == nil then
  68. module:log("error", "'app_id' must not be empty");
  69. return nil;
  70. end
  71. if self.appSecret == nil and self.asapKeyServer == nil then
  72. module:log("error", "'app_secret' or 'asap_key_server' must be specified");
  73. return nil;
  74. end
  75. --array of accepted issuers: by default only includes our appId
  76. self.acceptedIssuers = module:get_option_array('asap_accepted_issuers',{self.appId})
  77. --array of accepted audiences: by default only includes our appId
  78. self.acceptedAudiences = module:get_option_array('asap_accepted_audiences',{'*'})
  79. self.requireRoomClaim = module:get_option_boolean('asap_require_room_claim', true);
  80. if self.asapKeyServer and not have_async then
  81. module:log("error", "requires a version of Prosody with util.async");
  82. return nil;
  83. end
  84. return self
  85. end
  86. function Util:set_asap_key_server(asapKeyServer)
  87. self.asapKeyServer = asapKeyServer;
  88. end
  89. function Util:set_asap_accepted_issuers(acceptedIssuers)
  90. self.acceptedIssuers = acceptedIssuers;
  91. end
  92. function Util:set_asap_accepted_audiences(acceptedAudiences)
  93. self.acceptedAudiences = acceptedAudiences;
  94. end
  95. function Util:set_asap_require_room_claim(checkRoom)
  96. self.requireRoomClaim = checkRoom;
  97. end
  98. function Util:clear_asap_cache()
  99. self.cache = require"util.cache".new(cacheSize);
  100. end
  101. --- Returns the public key by keyID
  102. -- @param keyId the key ID to request
  103. -- @return the public key (the content of requested resource) or nil
  104. function Util:get_public_key(keyId)
  105. local content = self.cache:get(keyId);
  106. if content == nil then
  107. -- If the key is not found in the cache.
  108. module:log("debug", "Cache miss for key: "..keyId);
  109. local keyurl = path.join(self.asapKeyServer, hex.to(sha256(keyId))..'.pem');
  110. module:log("debug", "Fetching public key from: "..keyurl);
  111. content = http_get_with_retry(keyurl, nr_retries);
  112. if content ~= nil then
  113. self.cache:set(keyId, content);
  114. end
  115. return content;
  116. else
  117. -- If the key is in the cache, use it.
  118. module:log("debug", "Cache hit for key: "..keyId);
  119. return content;
  120. end
  121. end
  122. --- Verifies issuer part of token
  123. -- @param 'issClaim' claim from the token to verify
  124. -- @param 'acceptedIssuers' list of issuers to check
  125. -- @return nil and error string or true for accepted claim
  126. function Util:verify_issuer(issClaim, acceptedIssuers)
  127. if not acceptedIssuers then
  128. acceptedIssuers = self.acceptedIssuers
  129. end
  130. module:log("debug", "verify_issuer claim: %s against accepted: %s", issClaim, acceptedIssuers);
  131. for i, iss in ipairs(acceptedIssuers) do
  132. if iss == '*' then
  133. -- "*" indicates to accept any issuer in the claims so return success
  134. return true;
  135. end
  136. if issClaim == iss then
  137. -- claim matches an accepted issuer so return success
  138. return true;
  139. end
  140. end
  141. -- if issClaim not found in acceptedIssuers, fail claim
  142. return nil, "Invalid issuer ('iss' claim)";
  143. end
  144. --- Verifies audience part of token
  145. -- @param 'audClaim' claim from the token to verify
  146. -- @return nil and error string or true for accepted claim
  147. function Util:verify_audience(audClaim)
  148. module:log("debug", "verify_audience claim: %s against accepted: %s", audClaim, self.acceptedAudiences);
  149. for i, aud in ipairs(self.acceptedAudiences) do
  150. if aud == '*' then
  151. -- "*" indicates to accept any audience in the claims so return success
  152. return true;
  153. end
  154. if audClaim == aud then
  155. -- claim matches an accepted audience so return success
  156. return true;
  157. end
  158. end
  159. -- if audClaim not found in acceptedAudiences, fail claim
  160. return nil, "Invalid audience ('aud' claim)";
  161. end
  162. --- Verifies token
  163. -- @param token the token to verify
  164. -- @param secret the secret to use to verify token
  165. -- @param acceptedIssuers the list of accepted issuers to check
  166. -- @return nil and error or the extracted claims from the token
  167. function Util:verify_token(token, secret, acceptedIssuers)
  168. local claims, err = jwt.decode(token, secret, true);
  169. if claims == nil then
  170. return nil, err;
  171. end
  172. local alg = claims["alg"];
  173. if alg ~= nil and (alg == "none" or alg == "") then
  174. return nil, "'alg' claim must not be empty";
  175. end
  176. local issClaim = claims["iss"];
  177. if issClaim == nil then
  178. return nil, "'iss' claim is missing";
  179. end
  180. --check the issuer against the accepted list
  181. local issCheck, issCheckErr = self:verify_issuer(issClaim, acceptedIssuers);
  182. if issCheck == nil then
  183. return nil, issCheckErr;
  184. end
  185. if self.requireRoomClaim then
  186. local roomClaim = claims["room"];
  187. if roomClaim == nil then
  188. return nil, "'room' claim is missing";
  189. end
  190. end
  191. local audClaim = claims["aud"];
  192. if audClaim == nil then
  193. return nil, "'aud' claim is missing";
  194. end
  195. --check the audience against the accepted list
  196. local audCheck, audCheckErr = self:verify_audience(audClaim);
  197. if audCheck == nil then
  198. return nil, audCheckErr;
  199. end
  200. return claims;
  201. end
  202. --- Verifies token and process needed values to be stored in the session.
  203. -- Token is obtained from session.auth_token.
  204. -- Stores in session the following values:
  205. -- session.jitsi_meet_room - the room name value from the token
  206. -- session.jitsi_meet_domain - the domain name value from the token
  207. -- session.jitsi_meet_context_user - the user details from the token
  208. -- session.jitsi_meet_context_group - the group value from the token
  209. -- session.jitsi_meet_context_features - the features value from the token
  210. -- @param session the current session
  211. -- @param acceptedIssuers optional list of accepted issuers to check
  212. -- @return false and error
  213. function Util:process_and_verify_token(session, acceptedIssuers)
  214. if not acceptedIssuers then
  215. acceptedIssuers = self.acceptedIssuers;
  216. end
  217. if session.auth_token == nil then
  218. if self.allowEmptyToken then
  219. return true;
  220. else
  221. return false, "not-allowed", "token required";
  222. end
  223. end
  224. local pubKey;
  225. if session.public_key then
  226. module:log("debug","Public key was found on the session");
  227. pubKey = session.public_key;
  228. elseif self.asapKeyServer and session.auth_token ~= nil then
  229. local dotFirst = session.auth_token:find("%.");
  230. if not dotFirst then return nil, "Invalid token" end
  231. local header, err = json_safe.decode(basexx.from_url64(session.auth_token:sub(1,dotFirst-1)));
  232. if err then
  233. return false, "not-allowed", "bad token format";
  234. end
  235. local kid = header["kid"];
  236. if kid == nil then
  237. return false, "not-allowed", "'kid' claim is missing";
  238. end
  239. pubKey = self:get_public_key(kid);
  240. if pubKey == nil then
  241. return false, "not-allowed", "could not obtain public key";
  242. end
  243. end
  244. -- now verify the whole token
  245. local claims, msg;
  246. if self.asapKeyServer then
  247. claims, msg = self:verify_token(session.auth_token, pubKey, acceptedIssuers);
  248. else
  249. claims, msg = self:verify_token(session.auth_token, self.appSecret, acceptedIssuers);
  250. end
  251. if claims ~= nil then
  252. -- Binds room name to the session which is later checked on MUC join
  253. session.jitsi_meet_room = claims["room"];
  254. -- Binds domain name to the session
  255. session.jitsi_meet_domain = claims["sub"];
  256. -- Binds the user details to the session if available
  257. if claims["context"] ~= nil then
  258. if claims["context"]["user"] ~= nil then
  259. session.jitsi_meet_context_user = claims["context"]["user"];
  260. end
  261. if claims["context"]["group"] ~= nil then
  262. -- Binds any group details to the session
  263. session.jitsi_meet_context_group = claims["context"]["group"];
  264. end
  265. if claims["context"]["features"] ~= nil then
  266. -- Binds any features details to the session
  267. session.jitsi_meet_context_features = claims["context"]["features"];
  268. end
  269. if claims["context"]["room"] ~= nil then
  270. session.jitsi_meet_context_room = claims["context"]["room"]
  271. end
  272. end
  273. return true;
  274. else
  275. return false, "not-allowed", msg;
  276. end
  277. end
  278. --- Verifies room name and domain if necesarry.
  279. -- Checks configs and if necessary checks the room name extracted from
  280. -- room_address against the one saved in the session when token was verified.
  281. -- Also verifies domain name from token against the domain in the room_address,
  282. -- if enableDomainVerification is enabled.
  283. -- @param session the current session
  284. -- @param room_address the whole room address as received
  285. -- @return returns true in case room was verified or there is no need to verify
  286. -- it and returns false in case verification was processed
  287. -- and was not successful
  288. function Util:verify_room(session, room_address)
  289. if self.allowEmptyToken and session.auth_token == nil then
  290. module:log(
  291. "debug",
  292. "Skipped room token verification - empty tokens are allowed");
  293. return true;
  294. end
  295. -- extract room name using all chars, except the not allowed ones
  296. local room,_,_ = jid.split(room_address);
  297. if room == nil then
  298. log("error",
  299. "Unable to get name of the MUC room ? to: %s", room_address);
  300. return true;
  301. end
  302. local auth_room = string.lower(session.jitsi_meet_room);
  303. if not self.enableDomainVerification then
  304. -- if auth_room is missing, this means user is anonymous (no token for
  305. -- its domain) we let it through, jicofo is verifying creation domain
  306. if auth_room and room ~= auth_room and auth_room ~= '*' then
  307. return false;
  308. end
  309. return true;
  310. end
  311. local room_address_to_verify = jid.bare(room_address);
  312. local room_node = jid.node(room_address);
  313. -- parses bare room address, for multidomain expected format is:
  314. -- [subdomain]roomName@conference.domain
  315. local target_subdomain, target_room = extract_subdomain(room_node);
  316. -- if we have '*' as room name in token, this means all rooms are allowed
  317. -- so we will use the actual name of the room when constructing strings
  318. -- to verify subdomains and domains to simplify checks
  319. local room_to_check;
  320. if auth_room == '*' then
  321. -- authorized for accessing any room assign to room_to_check the actual
  322. -- room name
  323. if target_room ~= nil then
  324. -- we are in multidomain mode and we were able to extract room name
  325. room_to_check = target_room;
  326. else
  327. -- no target_room, room_address_to_verify does not contain subdomain
  328. -- so we get just the node which is the room name
  329. room_to_check = room_node;
  330. end
  331. else
  332. -- no wildcard, so check room against authorized room from the token
  333. if session.jitsi_meet_context_room and (session.jitsi_meet_context_room["regex"] == true or session.jitsi_meet_context_room["regex"] == "true") then
  334. if target_room ~= nil then
  335. -- room with subdomain
  336. room_to_check = target_room:match(auth_room);
  337. else
  338. room_to_check = room_node:match(auth_room);
  339. end
  340. else
  341. -- not a regex
  342. room_to_check = auth_room;
  343. end
  344. module:log("debug", "room to check: %s", room_to_check)
  345. if not room_to_check then
  346. return false
  347. end
  348. end
  349. local auth_domain = string.lower(session.jitsi_meet_domain);
  350. local subdomain_to_check;
  351. if target_subdomain then
  352. if auth_domain == '*' then
  353. -- check for wildcard in JWT claim, allow access if found
  354. subdomain_to_check = target_subdomain;
  355. else
  356. -- no wildcard in JWT claim, so check subdomain against sub in token
  357. subdomain_to_check = auth_domain;
  358. end
  359. -- from this point we depend on muc_domain_base,
  360. -- deny access if option is missing
  361. if not self.muc_domain_base then
  362. module:log("warn", "No 'muc_domain_base' option set, denying access!");
  363. return false;
  364. end
  365. return room_address_to_verify == jid.join(
  366. "["..subdomain_to_check.."]"..room_to_check, self.muc_domain);
  367. else
  368. if auth_domain == '*' then
  369. -- check for wildcard in JWT claim, allow access if found
  370. subdomain_to_check = self.muc_domain;
  371. else
  372. -- no wildcard in JWT claim, so check subdomain against sub in token
  373. subdomain_to_check = self.muc_domain_prefix.."."..auth_domain;
  374. end
  375. -- we do not have a domain part (multidomain is not enabled)
  376. -- verify with info from the token
  377. return room_address_to_verify == jid.join(room_to_check, subdomain_to_check);
  378. end
  379. end
  380. return Util;