From e58cad0464f1e086770db579d60ce58c6c2c13df Mon Sep 17 00:00:00 2001 From: damencho Date: Fri, 21 Jul 2023 13:55:03 -0500 Subject: [PATCH] feat: Adds option for a url to list of pub keys for jwt to pre-fetch. The URL is a link to a json file having a mapping kid -> public key. The mapping can contain also certificates, which will be used to get the public key. The list is being updated before the cache-control max-age header value is reached, if such a header is returned from the server. --- resources/prosody-plugins/token/util.lib.lua | 45 +++++++++++++++++++- resources/prosody-plugins/util.lib.lua | 16 +++++-- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/resources/prosody-plugins/token/util.lib.lua b/resources/prosody-plugins/token/util.lib.lua index d8832d59ac..4edc2ab5e8 100644 --- a/resources/prosody-plugins/token/util.lib.lua +++ b/resources/prosody-plugins/token/util.lib.lua @@ -13,8 +13,13 @@ local main_util = module:require "util"; local ends_with = main_util.ends_with; local http_get_with_retry = main_util.http_get_with_retry; local extract_subdomain = main_util.extract_subdomain; +local starts_with = main_util.starts_with; +local cjson_safe = require 'cjson.safe' +local timer = require "util.timer"; +local async = require "util.async"; local nr_retries = 3; +local ssl = require "ssl"; -- TODO: Figure out a less arbitrary default cache size. local cacheSize = module:get_option_number("jwt_pubkey_cache_size", 128); @@ -34,6 +39,9 @@ function Util.new(module) self.appId = module:get_option_string("app_id"); self.appSecret = module:get_option_string("app_secret"); self.asapKeyServer = module:get_option_string("asap_key_server"); + -- A URL that will return json file with a mapping between kids and public keys + -- If the response Cache-Control header we will respect it and refresh it + self.cacheKeysUrl = module:get_option_string("cache_keys_url"); self.signatureAlgorithm = module:get_option_string("signature_algorithm"); self.allowEmptyToken = module:get_option_boolean("allow_empty_token"); @@ -109,6 +117,35 @@ function Util.new(module) return nil; end + if self.cacheKeysUrl then + local update_keys_cache; + update_keys_cache = async.runner(function (name) + content, code, cache_for = http_get_with_retry(self.cacheKeysUrl, nr_retries); + if content ~= nil then + self.cachedKeys = {}; + -- Let's convert any certificate to public key + for k, v in pairs(cjson_safe.decode(content)) do + if starts_with(v, '-----BEGIN CERTIFICATE-----') then + self.cachedKeys[k] = ssl.loadcertificate(v):pubkey(); + end + end + + if cache_for then + cache_for = tonumber(cache_for); + -- let's schedule new update 60 seconds before the cache expiring + if cache_for > 60 then + cache_for = cache_for - 60; + end + timer.add_task(cache_for, function () + update_keys_cache:run("update_keys_cache"); + end); + end + + end + end); + update_keys_cache:run("update_keys_cache"); + end + return self end @@ -202,7 +239,13 @@ function Util:process_and_verify_token(session, acceptedIssuers) if alg.sub(alg,1,2) ~= "RS" then return false, "not-allowed", "'kid' claim only support with RS family"; end - key = self:get_public_key(kid); + + if self.cachedKeys and self.cachedKeys[kid] then + key = self.cachedKeys[kid]; + else + key = self:get_public_key(kid); + end + if key == nil then return false, "not-allowed", "could not obtain public key"; end diff --git a/resources/prosody-plugins/util.lib.lua b/resources/prosody-plugins/util.lib.lua index 3fdf6646c1..cd83f3a96d 100644 --- a/resources/prosody-plugins/util.lib.lua +++ b/resources/prosody-plugins/util.lib.lua @@ -291,7 +291,7 @@ end -- @returns result of the http call or nil if -- the external call failed after the last retry function http_get_with_retry(url, retry, auth_token) - local content, code; + local content, code, cache_for; local timeout_occurred; local wait, done = async.waiter(); local request_headers = http_headers or {} @@ -304,7 +304,17 @@ function http_get_with_retry(url, retry, auth_token) code = code_; if code == 200 or code == 204 then module:log("debug", "External call was successful, content %s", content_); - content = content_ + content = content_; + + -- if there is cache-control header, let's return the max-age value + if response_ and response_.headers and response_.headers['cache-control'] then + local vals = {}; + for k, v in response_.headers['cache-control']:gmatch('(%w+)=(%w+)') do + vals[k] = v; + end + -- max-age=123 will be parsed by the regex ^ to age=123 + cache_for = vals.age; + end else module:log("warn", "Error on GET request: Code %s, Content %s", code_, content_); @@ -351,7 +361,7 @@ function http_get_with_retry(url, retry, auth_token) timer.add_task(http_timeout, cancel); wait(); - return content, code; + return content, code, cache_for; end -- Checks whether there is status in the