diff --git a/lib/jwt/claims.rb b/lib/jwt/claims.rb index 66125954..55fda678 100644 --- a/lib/jwt/claims.rb +++ b/lib/jwt/claims.rb @@ -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' diff --git a/lib/jwt/claims/audience.rb b/lib/jwt/claims/audience.rb index ecd47ec3..5ca512c7 100644 --- a/lib/jwt/claims/audience.rb +++ b/lib/jwt/claims/audience.rb @@ -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 || ''}" if ([*aud] & [*expected_audience]).empty? end diff --git a/lib/jwt/claims/expiration.rb b/lib/jwt/claims/expiration.rb index 61f42346..071885ad 100644 --- a/lib/jwt/claims/expiration.rb +++ b/lib/jwt/claims/expiration.rb @@ -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') diff --git a/lib/jwt/claims/issued_at.rb b/lib/jwt/claims/issued_at.rb index 9c4c963a..6aaf5108 100644 --- a/lib/jwt/claims/issued_at.rb +++ b/lib/jwt/claims/issued_at.rb @@ -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') diff --git a/lib/jwt/claims/issuer.rb b/lib/jwt/claims/issuer.rb index 5c6a2fbe..f973e7f0 100644 --- a/lib/jwt/claims/issuer.rb +++ b/lib/jwt/claims/issuer.rb @@ -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 diff --git a/lib/jwt/claims/jwt_id.rb b/lib/jwt/claims/jwt_id.rb index d7c46062..9dc5b0d0 100644 --- a/lib/jwt/claims/jwt_id.rb +++ b/lib/jwt/claims/jwt_id.rb @@ -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) diff --git a/lib/jwt/claims/not_before.rb b/lib/jwt/claims/not_before.rb index 29dbe8df..e53f6de3 100644 --- a/lib/jwt/claims/not_before.rb +++ b/lib/jwt/claims/not_before.rb @@ -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') diff --git a/lib/jwt/claims/numeric.rb b/lib/jwt/claims/numeric.rb index f91ae222..c537b8f3 100644 --- a/lib/jwt/claims/numeric.rb +++ b/lib/jwt/claims/numeric.rb @@ -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[ @@ -19,7 +19,7 @@ def initialize(payload) @payload = payload.transform_keys(&:to_sym) end - def validate! + def verify! validate_numeric_claims true diff --git a/lib/jwt/claims/required.rb b/lib/jwt/claims/required.rb index 7049a491..4bdc9853 100644 --- a/lib/jwt/claims/required.rb +++ b/lib/jwt/claims/required.rb @@ -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) diff --git a/lib/jwt/claims/subject.rb b/lib/jwt/claims/subject.rb index 36c712ab..dd1df517 100644 --- a/lib/jwt/claims/subject.rb +++ b/lib/jwt/claims/subject.rb @@ -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 || ''}") unless sub.to_s == expected_subject end diff --git a/lib/jwt/encode.rb b/lib/jwt/encode.rb index 9d35a93f..3859c4cb 100644 --- a/lib/jwt/encode.rb +++ b/lib/jwt/encode.rb @@ -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 diff --git a/spec/jwt/claims/audience_spec.rb b/spec/jwt/claims/audience_spec.rb index 68605586..96f2326b 100644 --- a/spec/jwt/claims/audience_spec.rb +++ b/spec/jwt/claims/audience_spec.rb @@ -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' } diff --git a/spec/jwt/claims/expiration_spec.rb b/spec/jwt/claims/expiration_spec.rb index 4e7f75aa..f8638ad9 100644 --- a/spec/jwt/claims/expiration_spec.rb +++ b/spec/jwt/claims/expiration_spec.rb @@ -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 @@ -19,7 +19,7 @@ let(:leeway) { 10 } it 'passes validation' do - validate! + verify! end end @@ -27,14 +27,14 @@ 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 diff --git a/spec/jwt/claims/issued_at_spec.rb b/spec/jwt/claims/issued_at_spec.rb index 350ae027..c34e5e85 100644 --- a/spec/jwt/claims/issued_at_spec.rb +++ b/spec/jwt/claims/issued_at_spec.rb @@ -3,11 +3,11 @@ 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 @@ -15,14 +15,14 @@ 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 @@ -30,7 +30,7 @@ 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 @@ -38,7 +38,7 @@ let(:payload) { 'beautyexperts_nbf_iat' } it 'passes validation' do - validate! + verify! end end end diff --git a/spec/jwt/claims/issuer_spec.rb b/spec/jwt/claims/issuer_spec.rb index 8d1b7814..33d9470a 100644 --- a/spec/jwt/claims/issuer_spec.rb +++ b/spec/jwt/claims/issuer_spec.rb @@ -5,39 +5,39 @@ let(:payload) { { 'iss' => issuer } } let(:expected_issuers) { 'ruby-jwt-gem' } - subject(:validate!) { described_class.new(issuers: expected_issuers).validate!(context: JWT::Claims::ValidationContext.new(payload: payload)) } + subject(:verify!) { described_class.new(issuers: expected_issuers).verify!(context: JWT::Claims::VerificationContext.new(payload: payload)) } context 'when expected issuer is a string that matches the payload' do it 'passes validation' do - validate! + verify! end end context 'when expected issuer is a string that does not match the payload' do let(:issuer) { 'mismatched-issuer' } it 'raises JWT::InvalidIssuerError' do - expect { validate! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected ["ruby-jwt-gem"], received mismatched-issuer') + expect { verify! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected ["ruby-jwt-gem"], received mismatched-issuer') end end context 'when payload does not contain any issuer' do let(:payload) { {} } it 'raises JWT::InvalidIssuerError' do - expect { validate! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected ["ruby-jwt-gem"], received ') + expect { verify! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected ["ruby-jwt-gem"], received ') end end context 'when expected issuer is an array that matches the payload' do let(:expected_issuers) { ['first', issuer, 'third'] } it 'passes validation' do - validate! + verify! end end context 'when expected issuer is an array that does not match the payload' do let(:expected_issuers) { %w[first second] } it 'raises JWT::InvalidIssuerError' do - expect { validate! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected ["first", "second"], received ruby-jwt-gem') + expect { verify! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected ["first", "second"], received ruby-jwt-gem') end end @@ -45,7 +45,7 @@ let(:payload) { {} } let(:expected_issuers) { %w[first second] } it 'raises JWT::InvalidIssuerError' do - expect { validate! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected ["first", "second"], received ') + expect { verify! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected ["first", "second"], received ') end end @@ -53,7 +53,7 @@ let(:issuer) { 'ruby-jwt-gem' } let(:expected_issuers) { /\A(first|#{issuer}|third)\z/ } it 'passes validation' do - validate! + verify! end end @@ -61,7 +61,7 @@ let(:issuer) { 'mismatched-issuer' } let(:expected_issuers) { /\A(first|second)\z/ } it 'raises JWT::InvalidIssuerError' do - expect { validate! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected [/\A(first|second)\z/], received mismatched-issuer') + expect { verify! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected [/\A(first|second)\z/], received mismatched-issuer') end end @@ -69,7 +69,7 @@ let(:payload) { {} } let(:expected_issuers) { /\A(first|second)\z/ } it 'raises JWT::InvalidIssuerError' do - expect { validate! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected [/\A(first|second)\z/], received ') + expect { verify! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected [/\A(first|second)\z/], received ') end end @@ -77,7 +77,7 @@ let(:issuer) { 'ruby-jwt-gem' } let(:expected_issuers) { ->(iss) { iss.start_with?('ruby') } } it 'passes validation' do - validate! + verify! end end @@ -85,7 +85,7 @@ let(:issuer) { 'mismatched-issuer' } let(:expected_issuers) { ->(iss) { iss.start_with?('ruby') } } it 'raises JWT::InvalidIssuerError' do - expect { validate! }.to raise_error(JWT::InvalidIssuerError, /received mismatched-issuer/) + expect { verify! }.to raise_error(JWT::InvalidIssuerError, /received mismatched-issuer/) end end @@ -93,7 +93,7 @@ let(:payload) { {} } let(:expected_issuers) { ->(iss) { iss&.start_with?('ruby') } } it 'raises JWT::InvalidIssuerError' do - expect { validate! }.to raise_error(JWT::InvalidIssuerError, /received /) + expect { verify! }.to raise_error(JWT::InvalidIssuerError, /received /) end end @@ -106,7 +106,7 @@ def issuer_start_with_ruby?(issuer) let(:expected_issuers) { method(:issuer_start_with_ruby?) } it 'passes validation' do - validate! + verify! end end end diff --git a/spec/jwt/claims/jwt_id_spec.rb b/spec/jwt/claims/jwt_id_spec.rb index 1bfaf415..89db8a7c 100644 --- a/spec/jwt/claims/jwt_id_spec.rb +++ b/spec/jwt/claims/jwt_id_spec.rb @@ -5,58 +5,58 @@ let(:payload) { { 'jti' => jti } } let(:validator) { nil } - subject(:validate!) { described_class.new(validator: validator).validate!(context: JWT::Claims::ValidationContext.new(payload: payload)) } + subject(:verify!) { described_class.new(validator: validator).verify!(context: JWT::Claims::VerificationContext.new(payload: payload)) } context 'when payload contains a jti' do it 'passes validation' do - validate! + verify! end end context 'when payload is missing a jti' do let(:payload) { {} } it 'raises JWT::InvalidJtiError' do - expect { validate! }.to raise_error(JWT::InvalidJtiError, 'Missing jti') + expect { verify! }.to raise_error(JWT::InvalidJtiError, 'Missing jti') end end context 'when payload contains a jti that is an empty string' do let(:jti) { '' } it 'raises JWT::InvalidJtiError' do - expect { validate! }.to raise_error(JWT::InvalidJtiError, 'Missing jti') + expect { verify! }.to raise_error(JWT::InvalidJtiError, 'Missing jti') end end context 'when payload contains a jti that is a blank string' do let(:jti) { ' ' } it 'raises JWT::InvalidJtiError' do - expect { validate! }.to raise_error(JWT::InvalidJtiError, 'Missing jti') + expect { verify! }.to raise_error(JWT::InvalidJtiError, 'Missing jti') end end context 'when jti validator is a proc returning false' do let(:validator) { ->(_jti) { false } } it 'raises JWT::InvalidJtiError' do - expect { validate! }.to raise_error(JWT::InvalidJtiError, 'Invalid jti') + expect { verify! }.to raise_error(JWT::InvalidJtiError, 'Invalid jti') end end context 'when jti validator is a proc returning true' do let(:validator) { ->(_jti) { true } } it 'passes validation' do - validate! + verify! end end context 'when jti validator has 2 args' do let(:validator) { ->(_jti, _pl) { true } } it 'passes validation' do - validate! + verify! end end context 'when jti validator has 2 args' do it 'the second arg is the payload' do - described_class.new(validator: ->(_jti, pl) { expect(pl).to eq(payload) }).validate!(context: JWT::Claims::ValidationContext.new(payload: payload)) + described_class.new(validator: ->(_jti, pl) { expect(pl).to eq(payload) }).verify!(context: JWT::Claims::VerificationContext.new(payload: payload)) end end end diff --git a/spec/jwt/claims/not_before_spec.rb b/spec/jwt/claims/not_before_spec.rb index bd2621a0..1f8b4930 100644 --- a/spec/jwt/claims/not_before_spec.rb +++ b/spec/jwt/claims/not_before_spec.rb @@ -3,10 +3,10 @@ RSpec.describe JWT::Claims::NotBefore do let(:payload) { { 'nbf' => (Time.now.to_i + 5) } } - describe '#validate!' do + describe '#verify!' do context 'when nbf is in the future' do it 'raises JWT::ImmatureSignature' do - expect { described_class.new(leeway: 0).validate!(context: JWT::Claims::ValidationContext.new(payload: payload)) }.to raise_error JWT::ImmatureSignature + expect { described_class.new(leeway: 0).verify!(context: JWT::Claims::VerificationContext.new(payload: payload)) }.to raise_error JWT::ImmatureSignature end end @@ -14,13 +14,13 @@ let(:payload) { { 'nbf' => (Time.now.to_i - 5) } } it 'does not raise error' do - expect { described_class.new(leeway: 0).validate!(context: JWT::Claims::ValidationContext.new(payload: payload)) }.not_to raise_error + expect { described_class.new(leeway: 0).verify!(context: JWT::Claims::VerificationContext.new(payload: payload)) }.not_to raise_error end end context 'when leeway is given' do it 'does not raise error' do - expect { described_class.new(leeway: 10).validate!(context: JWT::Claims::ValidationContext.new(payload: payload)) }.not_to raise_error + expect { described_class.new(leeway: 10).verify!(context: JWT::Claims::VerificationContext.new(payload: payload)) }.not_to raise_error end end end diff --git a/spec/jwt/claims/numeric_spec.rb b/spec/jwt/claims/numeric_spec.rb index a4a00df4..6cef1251 100644 --- a/spec/jwt/claims/numeric_spec.rb +++ b/spec/jwt/claims/numeric_spec.rb @@ -3,8 +3,8 @@ RSpec.describe JWT::Claims::Numeric do let(:validator) { described_class.new(claims) } - describe '#validate!' do - subject { validator.validate! } + describe '#verify!' do + subject { validator.verify! } shared_examples_for 'a NumericDate claim' do |claim| context "when #{claim} payload is an integer" do diff --git a/spec/jwt/claims/required_spec.rb b/spec/jwt/claims/required_spec.rb index a32228de..97033460 100644 --- a/spec/jwt/claims/required_spec.rb +++ b/spec/jwt/claims/required_spec.rb @@ -3,12 +3,12 @@ RSpec.describe JWT::Claims::Required do let(:payload) { { 'data' => 'value' } } - subject(:validate!) { described_class.new(required_claims: required_claims).validate!(context: JWT::Claims::ValidationContext.new(payload: payload)) } + subject(:verify!) { described_class.new(required_claims: required_claims).verify!(context: JWT::Claims::VerificationContext.new(payload: payload)) } context 'when payload is missing the required claim' do let(:required_claims) { ['exp'] } it 'raises JWT::MissingRequiredClaim' do - expect { validate! }.to raise_error JWT::MissingRequiredClaim, 'Missing required claim exp' + expect { verify! }.to raise_error JWT::MissingRequiredClaim, 'Missing required claim exp' end end @@ -16,7 +16,7 @@ let(:payload) { { 'exp' => 'exp', 'custom_claim' => true } } let(:required_claims) { %w[exp custom_claim] } it 'passes validation' do - validate! + verify! end end end