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

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