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