mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 11:22:31 +00:00
feat(jwt): Supports JWKS endpoint. (#16649)
* feat(jwt): Supports JWKS endpoint. * squash: Allow setting just cache_keys_url.
This commit is contained in:
134
resources/prosody-plugins/token/jwk.lib.lua
Normal file
134
resources/prosody-plugins/token/jwk.lib.lua
Normal file
@@ -0,0 +1,134 @@
|
||||
local basexx = require "basexx";
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Helper function to encode bytes to base64
|
||||
function base64_encode(bytes)
|
||||
return basexx.to_base64(bytes)
|
||||
end
|
||||
|
||||
-- Pure Lua ASN.1 DER encoder (no external dependencies)
|
||||
local ASN1 = {}
|
||||
|
||||
-- Encode ASN.1 length field
|
||||
function ASN1.encode_length(len)
|
||||
if len < 128 then
|
||||
return string.char(len)
|
||||
elseif len < 256 then
|
||||
return string.char(0x81, len)
|
||||
elseif len < 65536 then
|
||||
return string.char(0x82, math.floor(len / 256), len % 256)
|
||||
else
|
||||
local b1 = math.floor(len / 65536)
|
||||
local b2 = math.floor((len % 65536) / 256)
|
||||
local b3 = len % 256
|
||||
return string.char(0x83, b1, b2, b3)
|
||||
end
|
||||
end
|
||||
|
||||
-- Encode ASN.1 INTEGER
|
||||
function ASN1.encode_integer(bytes)
|
||||
-- ASN.1 INTEGER tag is 0x02
|
||||
-- If the high bit is set, prepend 0x00 to indicate positive number
|
||||
if bytes:byte(1) >= 0x80 then
|
||||
bytes = string.char(0x00) .. bytes
|
||||
end
|
||||
return string.char(0x02) .. ASN1.encode_length(#bytes) .. bytes
|
||||
end
|
||||
|
||||
-- Encode ASN.1 SEQUENCE
|
||||
function ASN1.encode_sequence(content)
|
||||
-- ASN.1 SEQUENCE tag is 0x30
|
||||
return string.char(0x30) .. ASN1.encode_length(#content) .. content
|
||||
end
|
||||
|
||||
-- Encode ASN.1 BIT STRING
|
||||
function ASN1.encode_bit_string(content)
|
||||
-- ASN.1 BIT STRING tag is 0x03
|
||||
-- First byte indicates number of unused bits (0x00 for byte-aligned)
|
||||
return string.char(0x03) .. ASN1.encode_length(#content + 1) .. string.char(0x00) .. content
|
||||
end
|
||||
|
||||
-- Encode ASN.1 OBJECT IDENTIFIER
|
||||
function ASN1.encode_oid(oid_bytes)
|
||||
-- ASN.1 OID tag is 0x06
|
||||
return string.char(0x06) .. ASN1.encode_length(#oid_bytes) .. oid_bytes
|
||||
end
|
||||
|
||||
-- Encode ASN.1 NULL
|
||||
function ASN1.encode_null()
|
||||
-- ASN.1 NULL tag is 0x05, length 0
|
||||
return string.char(0x05, 0x00)
|
||||
end
|
||||
|
||||
-- Convert DER to PEM format
|
||||
function ASN1.der_to_pem(der, label)
|
||||
label = label or "PUBLIC KEY"
|
||||
local base64 = base64_encode(der)
|
||||
|
||||
-- Break into 64-character lines
|
||||
local lines = {}
|
||||
for i = 1, #base64, 64 do
|
||||
table.insert(lines, base64:sub(i, i + 63))
|
||||
end
|
||||
|
||||
return "-----BEGIN " .. label .. "-----\n" ..
|
||||
table.concat(lines, "\n") .. "\n" ..
|
||||
"-----END " .. label .. "-----\n"
|
||||
end
|
||||
|
||||
-- Helper function to decode base64url
|
||||
function base64url_decode(str)
|
||||
-- Convert base64url to base64
|
||||
str = str:gsub('-', '+'):gsub('_', '/')
|
||||
-- Add padding if needed
|
||||
local padding = #str % 4
|
||||
if padding > 0 then
|
||||
str = str .. string.rep('=', 4 - padding)
|
||||
end
|
||||
return basexx.from_base64(str)
|
||||
end
|
||||
|
||||
-- Helper function to convert JWK to PEM format
|
||||
function M.jwk_to_pem(jwk)
|
||||
-- Decode the modulus (n) and exponent (e) from base64url
|
||||
local n_bytes = base64url_decode(jwk.n)
|
||||
local e_bytes = base64url_decode(jwk.e)
|
||||
|
||||
-- Build RSA public key structure
|
||||
-- RSAPublicKey ::= SEQUENCE {
|
||||
-- modulus INTEGER, -- n
|
||||
-- publicExponent INTEGER -- e
|
||||
-- }
|
||||
local modulus_asn1 = ASN1.encode_integer(n_bytes)
|
||||
local exponent_asn1 = ASN1.encode_integer(e_bytes)
|
||||
local rsa_pubkey = ASN1.encode_sequence(modulus_asn1 .. exponent_asn1)
|
||||
|
||||
-- Build SubjectPublicKeyInfo structure
|
||||
-- SubjectPublicKeyInfo ::= SEQUENCE {
|
||||
-- algorithm AlgorithmIdentifier,
|
||||
-- subjectPublicKey BIT STRING
|
||||
-- }
|
||||
|
||||
-- RSA OID: 1.2.840.113549.1.1.1 (rsaEncryption)
|
||||
-- Encoded as: 06 09 2A 86 48 86 F7 0D 01 01 01
|
||||
local rsa_oid = string.char(0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01)
|
||||
local rsa_oid_encoded = ASN1.encode_oid(rsa_oid)
|
||||
|
||||
-- AlgorithmIdentifier ::= SEQUENCE {
|
||||
-- algorithm OBJECT IDENTIFIER,
|
||||
-- parameters NULL
|
||||
-- }
|
||||
local algorithm_id = ASN1.encode_sequence(rsa_oid_encoded .. ASN1.encode_null())
|
||||
|
||||
-- Wrap the RSA public key in a BIT STRING
|
||||
local subject_public_key = ASN1.encode_bit_string(rsa_pubkey)
|
||||
|
||||
-- Final SubjectPublicKeyInfo
|
||||
local spki = ASN1.encode_sequence(algorithm_id .. subject_public_key)
|
||||
|
||||
-- Convert to PEM format
|
||||
return ASN1.der_to_pem(spki, "PUBLIC KEY")
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -5,6 +5,7 @@ local basexx = require "basexx";
|
||||
local have_async, async = pcall(require, "util.async");
|
||||
local hex = require "util.hex";
|
||||
local jwt = module:require "luajwtjitsi";
|
||||
local jwk_to_pem = module:require "token/jwk".jwk_to_pem;
|
||||
local jid = require "util.jid";
|
||||
local json_safe = require "cjson.safe";
|
||||
local path = require "util.paths";
|
||||
@@ -111,14 +112,14 @@ function Util.new(module)
|
||||
return nil;
|
||||
end
|
||||
|
||||
if self.appSecret == nil and self.asapKeyServer == nil then
|
||||
module:log("error", "'app_secret' or 'asap_key_server' must be specified");
|
||||
if self.appSecret == nil and self.asapKeyServer == nil and self.cacheKeysUrl == nil then
|
||||
module:log("error", "'app_secret', 'asap_key_server or 'cacheKeysUrl' must be specified");
|
||||
return nil;
|
||||
end
|
||||
|
||||
-- Set defaults for signature algorithm
|
||||
if self.signatureAlgorithm == nil then
|
||||
if self.asapKeyServer ~= nil then
|
||||
if self.asapKeyServer ~= nil or self.cacheKeysUrl then
|
||||
self.signatureAlgorithm = "RS256"
|
||||
elseif self.appSecret ~= nil then
|
||||
self.signatureAlgorithm = "HS256"
|
||||
@@ -133,7 +134,7 @@ function Util.new(module)
|
||||
|
||||
self.requireRoomClaim = module:get_option_boolean('asap_require_room_claim', true);
|
||||
|
||||
if self.asapKeyServer and not have_async then
|
||||
if (self.asapKeyServer or self.cacheKeysUrl) and not have_async then
|
||||
module:log("error", "requires a version of Prosody with util.async");
|
||||
return nil;
|
||||
end
|
||||
@@ -148,7 +149,18 @@ function Util.new(module)
|
||||
local keys_to_delete = table_shallow_copy(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
|
||||
-- JWKS format
|
||||
if k == "keys" and type(v) == "table" then
|
||||
for _, key in ipairs(v) do
|
||||
if key.kid then
|
||||
self.cachedKeys[key.kid] = jwk_to_pem(key);
|
||||
|
||||
-- do not clean this key if it already exists
|
||||
keys_to_delete[key.kid] = nil;
|
||||
end
|
||||
end
|
||||
-- direct PEM mapping (Firebase)
|
||||
elseif starts_with(v, '-----BEGIN CERTIFICATE-----') then
|
||||
self.cachedKeys[k] = ssl.loadcertificate(v):pubkey();
|
||||
-- do not clean this key if it already exists
|
||||
keys_to_delete[k] = nil;
|
||||
@@ -263,7 +275,7 @@ function Util:process_and_verify_token(session)
|
||||
-- We're using an public key stored in the session
|
||||
-- module:log("debug","Public key was found on the session");
|
||||
key = session.public_key;
|
||||
elseif self.asapKeyServer and session.auth_token ~= nil then
|
||||
elseif (self.asapKeyServer or self.cacheKeysUrl) and session.auth_token ~= nil then
|
||||
-- We're fetching an public key from an ASAP server
|
||||
local dotFirst = session.auth_token:find("%.");
|
||||
if not dotFirst then return false, "not-allowed", "Invalid token" end
|
||||
|
||||
Reference in New Issue
Block a user