"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;