"Fossies" - the Fresh Open Source Software Archive

Member "jitsi-meet-5086/resources/prosody-plugins/token/util.lib.lua" (22 Jun 2021, 16658 Bytes) of package /linux/misc/jitsi-meet-5086.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. See also the last Fossies "Diffs" side-by-side code changes report for "util.lib.lua": jitsi-meet_5870_vs_jitsi-meet_5963.

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