Skip to content

Commit

Permalink
fix: Cache keyset for decoding
Browse files Browse the repository at this point in the history
Cache keyset for decoding
  • Loading branch information
KoenSengers committed Aug 2, 2024
1 parent b078859 commit cb51b3c
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 6 deletions.
2 changes: 1 addition & 1 deletion keypairs.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
18 changes: 16 additions & 2 deletions lib/keypair.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -133,13 +133,27 @@ def self.jwt_decode(id_token, options = {})
# Change the default algorithm to match the encoding algorithm
algorithm: ALGORITHM,
# Load our own keyset as valid keys
jwks: keyset,
jwks: jwk_loader_cached,
# If the `sub` is provided, validate that it matches the payload `sub`
verify_sub: true
)
JWT.decode(id_token, nil, true, options).first.with_indifferent_access
end

# options[:invalidate] will be `true` if a matching `kid` was not found
# https://github.com/jwt/ruby-jwt/blob/master/lib/jwt/jwk/key_finder.rb#L31
def self.jwk_loader_cached
lambda do |options|
cached_jwks(force: options[:invalidate]) || {}
end
end

def self.cached_jwks(force: false)
Rails.cache.fetch('keypairs/Keypair/jwks', force: force, skip_nil: true) do
keyset
end
end

# JWT encodes the payload with this keypair.
# It automatically adds the security attributes +iat+, +exp+ and +nonce+ to the payload.
# It automatically sets the +kid+ in the header.
Expand Down
2 changes: 1 addition & 1 deletion lib/keypairs/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Keypairs
VERSION = '2.0.0.develop'
VERSION = '1.3.4.develop'
end
16 changes: 16 additions & 0 deletions spec/models/keypair_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,22 @@
expect { subject }.to raise_error JWT::DecodeError
end
end

context 'should update cache if kid is not regonized' do
let(:other_payload) { { uuid: SecureRandom.uuid } }
let(:other_id_token) { other_keypair.jwt_encode(other_payload) }
let(:other_keypair) { described_class.create! }
let(:keypair) { described_class.current }
subject { described_class.jwt_decode(other_id_token).deep_symbolize_keys }

before do
old_keypair = described_class.create!
token = old_keypair.jwt_encode({ uuid: SecureRandom.uuid })
described_class.jwt_decode(token)
end

it { expect(subject[:uuid]).to eq other_payload[:uuid] }
end
end

describe '#public_jwk_export' do
Expand Down
4 changes: 2 additions & 2 deletions spec/support/matchers/encrypt_attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit cb51b3c

Please sign in to comment.