Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

util.lib.lua 15KB

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