Don't forget to lycansubscribe

extracted JWT authentication to a new class

Changed files
+61 -59
app
+55
app/authenticator.rb
··· 1 + require 'base58' 2 + require 'base64' 3 + require 'didkit' 4 + require 'json' 5 + require 'jwt' 6 + require 'openssl' 7 + 8 + class Authenticator 9 + def initialize(hostname:) 10 + @@pkey_cache ||= {} 11 + @hostname = hostname 12 + end 13 + 14 + def decode_user_from_jwt(auth_header, endpoint) 15 + return nil unless auth_header.start_with?('Bearer ') 16 + 17 + token = auth_header.gsub(/\ABearer /, '') 18 + data = JSON.parse(Base64.decode64(token.split('.')[1])) 19 + did = data['iss'] 20 + return nil if data['aud'] != "did:web:#{@hostname}" || data['lxm'] != endpoint 21 + 22 + pkey = pkey_for_user(did) 23 + 24 + decoded = JWT.decode(token, pkey, true, { algorithm: 'ES256K' }) 25 + decoded[0] && decoded[0]['iss'] 26 + end 27 + 28 + def pkey_for_user(did) 29 + # I have no idea what this does, but it seems to be working ¯\_(ツ)_/¯ 30 + 31 + if pkey = @@pkey_cache[did] 32 + return pkey 33 + end 34 + 35 + doc = DID.new(did).document.json 36 + key_obj = (doc['verificationMethod'] || []).detect { |x| x['type'] == 'Multikey' } 37 + key_multi = key_obj&.dig('publicKeyMultibase') 38 + return nil unless key_multi 39 + 40 + key_decoded = Base58.base58_to_binary(key_multi[1..], :bitcoin) 41 + comp_key = key_decoded[2..-1] 42 + 43 + alg_id = OpenSSL::ASN1::Sequence([ 44 + OpenSSL::ASN1::ObjectId('id-ecPublicKey'), 45 + OpenSSL::ASN1::ObjectId('secp256k1') 46 + ]) 47 + 48 + der = OpenSSL::ASN1::Sequence([alg_id, OpenSSL::ASN1::BitString(comp_key)]).to_der 49 + pkey = OpenSSL::PKey.read(der) 50 + 51 + @@pkey_cache[did] = pkey 52 + 53 + pkey 54 + end 55 + end
+6 -59
app/server.rb
··· 1 - require 'base58' 2 - require 'base64' 3 - require 'didkit' 4 1 require 'json' 5 - require 'jwt' 6 - require 'openssl' 7 2 require 'sinatra/base' 8 3 9 4 require_relative 'init' 5 + require_relative 'authenticator' 10 6 require_relative 'models/user' 11 7 require_relative 'query_parser' 12 8 13 9 class Server < Sinatra::Application 14 - class PKeyCache 15 - def self.get(did) 16 - @cache ||= {} 17 - @cache[did] 18 - end 19 - 20 - def self.set(did, pkey) 21 - @cache ||= {} 22 - @cache[did] = pkey 23 - end 24 - end 25 - 26 10 register Sinatra::ActiveRecordExtension 27 11 set :port, 3000 28 12 ··· 39 23 content_type :json 40 24 [status, JSON.generate({ error: name, message: message })] 41 25 end 42 - 43 - def decode_user_from_jwt(auth_header, endpoint) 44 - return nil unless auth_header.start_with?('Bearer ') 26 + end 45 27 46 - token = auth_header.gsub(/\ABearer /, '') 47 - data = JSON.parse(Base64.decode64(token.split('.')[1])) 48 - did = data['iss'] 49 - return nil if data['aud'] != "did:web:#{HOSTNAME}" || data['lxm'] != endpoint 50 - 51 - pkey = pkey_for_user(did) 52 - 53 - decoded = JWT.decode(token, pkey, true, { algorithm: 'ES256K' }) 54 - decoded[0] && decoded[0]['iss'] 55 - end 56 - 57 - def pkey_for_user(did) 58 - # I have no idea what this does, but it seems to be working ¯\_(ツ)_/¯ 59 - 60 - if pkey = PKeyCache.get(did) 61 - return pkey 62 - end 63 - 64 - doc = DID.new(did).document.json 65 - key_obj = (doc['verificationMethod'] || []).detect { |x| x['type'] == 'Multikey' } 66 - key_multi = key_obj&.dig('publicKeyMultibase') 67 - return nil unless key_multi 68 - 69 - key_decoded = Base58.base58_to_binary(key_multi[1..], :bitcoin) 70 - comp_key = key_decoded[2..-1] 71 - 72 - alg_id = OpenSSL::ASN1::Sequence([ 73 - OpenSSL::ASN1::ObjectId('id-ecPublicKey'), 74 - OpenSSL::ASN1::ObjectId('secp256k1') 75 - ]) 76 - 77 - der = OpenSSL::ASN1::Sequence([alg_id, OpenSSL::ASN1::BitString(comp_key)]).to_der 78 - pkey = OpenSSL::PKey.read(der) 79 - 80 - PKeyCache.set(did, pkey) 81 - 82 - pkey 83 - end 28 + before do 29 + @authenticator = Authenticator.new(hostname: HOSTNAME) 84 30 end 85 31 86 32 get '/xrpc/blue.feeds.lycan.searchPosts' do ··· 91 37 return json_error('UserNotFound', 'Missing "user" parameter') if user.nil? 92 38 else 93 39 begin 94 - did = decode_user_from_jwt(env['HTTP_AUTHORIZATION'], 'blue.feeds.lycan.searchPosts') 40 + auth_header = env['HTTP_AUTHORIZATION'] 41 + did = @authenticator.decode_user_from_jwt(auth_header, 'blue.feeds.lycan.searchPosts') 95 42 rescue StandardError => e 96 43 p e 97 44 end