Skip to content

Commit

Permalink
remove duplication in claim verifications
Browse files Browse the repository at this point in the history
  • Loading branch information
MatteoPierro authored and anakinj committed Aug 4, 2024
1 parent 3a1809e commit 9710bc9
Show file tree
Hide file tree
Showing 19 changed files with 83 additions and 124 deletions.
93 changes: 26 additions & 67 deletions lib/jwt/claims.rb
Original file line number Diff line number Diff line change
@@ -1,79 +1,38 @@
# frozen_string_literal: true

require_relative 'claims/audience'
require_relative 'claims/expiration'
require_relative 'claims/issued_at'
require_relative 'claims/issuer'
require_relative 'claims/jwt_id'
require_relative 'claims/not_before'
require_relative 'claims/numeric'
require_relative 'claims/required'
require_relative 'claims/subject'

module JWT
module Claims
ValidationContext = Struct.new(:payload, keyword_init: true)
VerificationContext = Struct.new(:payload, keyword_init: true)

VERIFIERS = {
verify_expiration: ->(options) { Claims::Expiration.new(leeway: options[:exp_leeway] || options[:leeway]) },
verify_not_before: ->(options) { Claims::NotBefore.new(leeway: options[:nbf_leeway] || options[:leeway]) },
verify_iss: ->(options) { Claims::Issuer.new(issuers: options[:iss]) },
verify_iat: ->(*) { Claims::IssuedAt.new },
verify_jti: ->(options) { Claims::JwtId.new(validator: options[:verify_jti]) },
verify_aud: ->(options) { Claims::Audience.new(expected_audience: options[:aud]) },
verify_sub: ->(options) { options[:sub] && Claims::Subject.new(expected_subject: options[:sub]) },
required_claims: ->(options) { Claims::Required.new(required_claims: options[:required_claims]) }
}.freeze

class << self
def verify!(payload, options)
verify_expiration(payload, options)
verify_not_before(payload, options)
verify_iss(payload, options)
verify_iat(payload, options)
verify_jti(payload, options)
verify_aud(payload, options)
verify_sub(payload, options)
verify_required_claims(payload, options)
end
VERIFIERS.each do |key, verifier_builder|
next unless options[key]

def verify_aud(payload, options)
return unless options[:verify_aud]

Claims::Audience.new(expected_audience: options[:aud]).validate!(context: ValidationContext.new(payload: payload))
end

def verify_expiration(payload, options)
return unless options[:verify_expiration]

Claims::Expiration.new(leeway: options[:exp_leeway] || options[:leeway]).validate!(context: ValidationContext.new(payload: payload))
end

def verify_iat(payload, options)
return unless options[:verify_iat]

Claims::IssuedAt.new.validate!(context: ValidationContext.new(payload: payload))
end

def verify_iss(payload, options)
return unless options[:verify_iss]

Claims::Issuer.new(issuers: options[:iss]).validate!(context: ValidationContext.new(payload: payload))
end

def verify_jti(payload, options)
return unless options[:verify_jti]

Claims::JwtId.new(validator: options[:verify_jti]).validate!(context: ValidationContext.new(payload: payload))
end

def verify_not_before(payload, options)
return unless options[:verify_not_before]

Claims::NotBefore.new(leeway: options[:nbf_leeway] || options[:leeway]).validate!(context: ValidationContext.new(payload: payload))
end

def verify_sub(payload, options)
return unless options[:verify_sub]
return unless options[:sub]

Claims::Subject.new(expected_subject: options[:sub]).validate!(context: ValidationContext.new(payload: payload))
end

def verify_required_claims(payload, options)
return unless (options_required_claims = options[:required_claims])

Claims::Required.new(required_claims: options_required_claims).validate!(context: ValidationContext.new(payload: payload))
verifier_builder&.call(options)&.verify!(context: VerificationContext.new(payload: payload))
end
end
end
end
end

require_relative 'claims/audience'
require_relative 'claims/expiration'
require_relative 'claims/issued_at'
require_relative 'claims/issuer'
require_relative 'claims/jwt_id'
require_relative 'claims/not_before'
require_relative 'claims/numeric'
require_relative 'claims/required'
require_relative 'claims/subject'
2 changes: 1 addition & 1 deletion lib/jwt/claims/audience.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def initialize(expected_audience:)
@expected_audience = expected_audience
end

def validate!(context:, **_args)
def verify!(context:, **_args)
aud = context.payload['aud']
raise JWT::InvalidAudError, "Invalid audience. Expected #{expected_audience}, received #{aud || '<none>'}" if ([*aud] & [*expected_audience]).empty?
end
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/claims/expiration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def initialize(leeway:)
@leeway = leeway || 0
end

def validate!(context:, **_args)
def verify!(context:, **_args)
return unless context.payload.is_a?(Hash)
return unless context.payload.key?('exp')

Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/claims/issued_at.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module JWT
module Claims
class IssuedAt
def validate!(context:, **_args)
def verify!(context:, **_args)
return unless context.payload.is_a?(Hash)
return unless context.payload.key?('iat')

Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/claims/issuer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def initialize(issuers:)
@issuers = Array(issuers).map { |item| item.is_a?(Symbol) ? item.to_s : item }
end

def validate!(context:, **_args)
def verify!(context:, **_args)
case (iss = context.payload['iss'])
when *issuers
nil
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/claims/jwt_id.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def initialize(validator:)
@validator = validator
end

def validate!(context:, **_args)
def verify!(context:, **_args)
jti = context.payload['jti']
if validator.respond_to?(:call)
verified = validator.arity == 2 ? validator.call(jti, context.payload) : validator.call(jti)
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/claims/not_before.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def initialize(leeway:)
@leeway = leeway || 0
end

def validate!(context:, **_args)
def verify!(context:, **_args)
return unless context.payload.is_a?(Hash)
return unless context.payload.key?('nbf')

Expand Down
6 changes: 3 additions & 3 deletions lib/jwt/claims/numeric.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
module JWT
module Claims
class Numeric
def self.validate!(payload:, **_args)
def self.verify!(payload:, **_args)
return unless payload.is_a?(Hash)

new(payload).validate!
new(payload).verify!
end

NUMERIC_CLAIMS = %i[
Expand All @@ -19,7 +19,7 @@ def initialize(payload)
@payload = payload.transform_keys(&:to_sym)
end

def validate!
def verify!
validate_numeric_claims

true
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/claims/required.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def initialize(required_claims:)
@required_claims = required_claims
end

def validate!(context:, **_args)
def verify!(context:, **_args)
required_claims.each do |required_claim|
next if context.payload.is_a?(Hash) && context.payload.include?(required_claim)

Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/claims/subject.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def initialize(expected_subject:)
@expected_subject = expected_subject.to_s
end

def validate!(context:, **_args)
def verify!(context:, **_args)
sub = context.payload['sub']
raise(JWT::InvalidSubError, "Invalid subject. Expected #{expected_subject}, received #{sub || '<none>'}") unless sub.to_s == expected_subject
end
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/encode.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def signature
def validate_claims!
return unless @payload.is_a?(Hash)

Claims::Numeric.new(@payload).validate!
Claims::Numeric.new(@payload).verify!
end

def encode_signature
Expand Down
4 changes: 2 additions & 2 deletions spec/jwt/claims/audience_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
RSpec.describe JWT::Claims::Audience do
let(:payload) { { 'nbf' => (Time.now.to_i + 5) } }

describe '#validate!' do
describe '#verify!' do
let(:scalar_aud) { 'ruby-jwt-aud' }
let(:array_aud) { %w[ruby-jwt-aud test-aud ruby-ruby-ruby] }

subject(:validate!) { described_class.new(expected_audience: expected_audience).validate!(context: JWT::Claims::ValidationContext.new(payload: payload)) }
subject(:verify!) { described_class.new(expected_audience: expected_audience).verify!(context: JWT::Claims::VerificationContext.new(payload: payload)) }

context 'when the singular audience does not match' do
let(:expected_audience) { 'no-match' }
Expand Down
10 changes: 5 additions & 5 deletions spec/jwt/claims/expiration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
let(:payload) { { 'exp' => (Time.now.to_i + 5) } }
let(:leeway) { 0 }

subject(:validate!) { described_class.new(leeway: leeway).validate!(context: JWT::Claims::ValidationContext.new(payload: payload)) }
subject(:verify!) { described_class.new(leeway: leeway).verify!(context: JWT::Claims::VerificationContext.new(payload: payload)) }

context 'when token is expired' do
let(:payload) { { 'exp' => (Time.now.to_i - 5) } }

it 'must raise JWT::ExpiredSignature when the token has expired' do
expect { validate! }.to(raise_error(JWT::ExpiredSignature))
expect { verify! }.to(raise_error(JWT::ExpiredSignature))
end
end

Expand All @@ -19,22 +19,22 @@
let(:leeway) { 10 }

it 'passes validation' do
validate!
verify!
end
end

context 'when token exp is set to current time' do
let(:payload) { { 'exp' => Time.now.to_i } }

it 'fails validation' do
expect { validate! }.to(raise_error(JWT::ExpiredSignature))
expect { verify! }.to(raise_error(JWT::ExpiredSignature))
end
end

context 'when token is not a Hash' do
let(:payload) { 'beautyexperts_nbf_iat' }
it 'passes validation' do
validate!
verify!
end
end
end
12 changes: 6 additions & 6 deletions spec/jwt/claims/issued_at_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,42 @@
RSpec.describe JWT::Claims::IssuedAt do
let(:payload) { { 'iat' => Time.now.to_f } }

subject(:validate!) { described_class.new.validate!(context: JWT::Claims::ValidationContext.new(payload: payload)) }
subject(:verify!) { described_class.new.verify!(context: JWT::Claims::VerificationContext.new(payload: payload)) }

context 'when iat is now' do
it 'passes validation' do
validate!
verify!
end
end

context 'when iat is now as a integer' do
let(:payload) { { 'iat' => Time.now.to_i } }

it 'passes validation' do
validate!
verify!
end
end
context 'when iat is not a number' do
let(:payload) { { 'iat' => 'not_a_number' } }

it 'fails validation' do
expect { validate! }.to raise_error(JWT::InvalidIatError)
expect { verify! }.to raise_error(JWT::InvalidIatError)
end
end

context 'when iat is in the future' do
let(:payload) { { 'iat' => Time.now.to_f + 120.0 } }

it 'fails validation' do
expect { validate! }.to raise_error(JWT::InvalidIatError)
expect { verify! }.to raise_error(JWT::InvalidIatError)
end
end

context 'when payload is a string containing iat' do
let(:payload) { 'beautyexperts_nbf_iat' }

it 'passes validation' do
validate!
verify!
end
end
end
Loading

0 comments on commit 9710bc9

Please sign in to comment.