From d83d28d49ceca83e947b19139970c610da016c3c Mon Sep 17 00:00:00 2001 From: Koen Sengers Date: Fri, 2 Aug 2024 09:30:34 +0200 Subject: [PATCH] fix: Cache keyset for decoding Cache keyset for decoding --- keypairs.gemspec | 2 +- lib/keypair.rb | 21 +++++++++++++++++++-- spec/support/matchers/encrypt_attribute.rb | 4 ++-- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/keypairs.gemspec b/keypairs.gemspec index b8a6d72..3e46dad 100644 --- a/keypairs.gemspec +++ b/keypairs.gemspec @@ -39,6 +39,6 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'rubocop-performance' # Linter for Performance optimization analysis spec.add_development_dependency 'rubocop-rails' # Linter for Rails-specific analysis spec.add_development_dependency 'shoulda-matchers' # RSpec matchers - spec.add_development_dependency 'sqlite3' # Database adapter + spec.add_development_dependency 'sqlite3', '~> 1.4' # Database adapter spec.add_development_dependency 'timecop' # Freeze time to test time-dependent code end diff --git a/lib/keypair.rb b/lib/keypair.rb index 38e19c0..c49238d 100644 --- a/lib/keypair.rb +++ b/lib/keypair.rb @@ -34,7 +34,7 @@ # @attr [Time] not_before The time before which no payloads may be signed using the keypair. # @attr [Time] not_after The time after which no payloads may be signed using the keypair. # @attr [Time] expires_at The time after which the keypair may not be used for signature validation. -class Keypair < ActiveRecord::Base +class Keypair < ActiveRecord::Base # rubocop:disable Metrics/ClassLength ALGORITHM = 'RS256' ROTATION_INTERVAL = 1.month @@ -128,12 +128,15 @@ def self.jwt_encode_without_nonce(payload) # @raise [JWT::DecodeError] or any of it's subclasses if the decoding / validation fails. # @return [Hash] Decoded payload hash with indifferent access. def self.jwt_decode(id_token, options = {}) + decoding_keyset = Rails.cache.fetch('keypairs/Keypair/keyset_decode', expires_in: decode_cache_expires_in) do + keyset + end # Add default decoding options options.reverse_merge!( # Change the default algorithm to match the encoding algorithm algorithm: ALGORITHM, # Load our own keyset as valid keys - jwks: keyset, + jwks: decoding_keyset, # If the `sub` is provided, validate that it matches the payload `sub` verify_sub: true ) @@ -245,4 +248,18 @@ def secure_payload(payload, nonce: true) payload.reverse_merge!(secure_payload) end + + class << self + def decode_cache_expires_in + expiring_time = first_expiring_or_not_before_key + expiring_time ? expiring_time - Time.zone.now : nil + end + + def first_expiring_or_not_before_key + valid_keys = valid + expires_in_key = valid_keys.minimum(:expires_at) + not_before_key = valid_keys.where(arel_table[:not_before].gt(Time.zone.now)).minimum(:not_before) + [expires_in_key, not_before_key].compact.min + end + end end diff --git a/spec/support/matchers/encrypt_attribute.rb b/spec/support/matchers/encrypt_attribute.rb index b046207..e0b823c 100644 --- a/spec/support/matchers/encrypt_attribute.rb +++ b/spec/support/matchers/encrypt_attribute.rb @@ -6,9 +6,9 @@ match do |model| # Correct responds to methods model.respond_to?(attribute) && - model.respond_to?("#{attribute}=") && + model.respond_to?(:"#{attribute}=") && model.respond_to?(database_column_name) && - model.respond_to?("#{database_column_name}=") && + model.respond_to?(:"#{database_column_name}=") && # Correct database columns model.class.column_names.exclude?(attribute.to_s) && model.class.column_names.include?(database_column_name)