diff --git a/.gitignore b/.gitignore index 04ad34a0..4069d061 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ coverage/ *gemfile.lock .byebug_history *.gem +doc/ diff --git a/CHANGELOG.md b/CHANGELOG.md index cf05db0f..1f0edc56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Updated README to correctly document `OpenSSL::HMAC` documentation [#617](https://github.com/jwt/ruby-jwt/pull/617) ([@aedryan](https://github.com/aedryan)) - Verify JWT header format [#622](https://github.com/jwt/ruby-jwt/pull/622) ([@304](https://github.com/304)) +- Bring back `::JWT::ClaimsValidator`, `::JWT::Verify` and a few other removed interfaces for preserved backwards compatibility [#624](https://github.com/jwt/ruby-jwt/pull/624) ([@anakinj](https://github.com/anakinj)) - Your contribution here ## [v2.9.1](https://github.com/jwt/ruby-jwt/tree/v2.9.1) (2024-09-23) diff --git a/lib/jwt.rb b/lib/jwt.rb index f7f28d47..dd5cdff5 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -11,6 +11,9 @@ require 'jwt/jwk' require 'jwt/claims' +require 'jwt/claims_validator' +require 'jwt/verify' + # JSON Web Token implementation # # Should be up to date with the latest spec: diff --git a/lib/jwt/claims.rb b/lib/jwt/claims.rb index 9bb71488..199af0a6 100644 --- a/lib/jwt/claims.rb +++ b/lib/jwt/claims.rb @@ -28,10 +28,11 @@ module Claims class << self def verify!(payload, options) VERIFIERS.each do |key, verifier_builder| - next unless options[key] + next unless options[key] || options[key.to_s] verifier_builder&.call(options)&.verify!(context: VerificationContext.new(payload: payload)) end + nil end end end diff --git a/lib/jwt/claims_validator.rb b/lib/jwt/claims_validator.rb new file mode 100644 index 00000000..70fee31b --- /dev/null +++ b/lib/jwt/claims_validator.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module JWT + class ClaimsValidator + def initialize(payload) + Deprecations.warning('The ::JWT::ClaimsValidator class is deprecated and will be removed in the next major version of ruby-jwt') + @payload = payload + end + + def validate! + Claims::Numeric.verify!(payload: @payload) + end + end +end diff --git a/lib/jwt/jwa.rb b/lib/jwt/jwa.rb index 8a975824..b069f98f 100644 --- a/lib/jwt/jwa.rb +++ b/lib/jwt/jwa.rb @@ -8,6 +8,7 @@ raise if defined?(RbNaCl) end +require_relative 'jwa/compat' require_relative 'jwa/signing_algorithm' require_relative 'jwa/ecdsa' require_relative 'jwa/hmac' @@ -34,12 +35,17 @@ def resolve(algorithm) return find(algorithm) if algorithm.is_a?(String) || algorithm.is_a?(Symbol) unless algorithm.is_a?(SigningAlgorithm) - Deprecations.warning('Custom algorithms are required to include JWT::JWA::SigningAlgorithm') + Deprecations.warning('Custom algorithms are required to include JWT::JWA::SigningAlgorithm. Custom algorithms that do not include this module may stop working in the next major version of ruby-jwt.') return Wrapper.new(algorithm) end algorithm end + + def create(algorithm) + Deprecations.warning('The ::JWT::JWA.create method is deprecated and will be removed in the next major version of ruby-jwt.') + resolve(algorithm) + end end end end diff --git a/lib/jwt/jwa/compat.rb b/lib/jwt/jwa/compat.rb new file mode 100644 index 00000000..297ced69 --- /dev/null +++ b/lib/jwt/jwa/compat.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module JWT + module JWA + module Compat + module ClassMethods + def from_algorithm(algorithm) + new(algorithm) + end + + def sign(algorithm, msg, key) + Deprecations.warning('Support for calling sign with positional arguments will be removed in future ruby-jwt versions') + + from_algorithm(algorithm).sign(data: msg, signing_key: key) + end + + def verify(algorithm, key, signing_input, signature) + Deprecations.warning('Support for calling verify with positional arguments will be removed in future ruby-jwt versions') + + from_algorithm(algorithm).verify(data: signing_input, signature: signature, verification_key: key) + end + end + + def self.included(klass) + klass.extend(ClassMethods) + end + end + end +end diff --git a/lib/jwt/jwa/ecdsa.rb b/lib/jwt/jwa/ecdsa.rb index abc7246e..076d28f1 100644 --- a/lib/jwt/jwa/ecdsa.rb +++ b/lib/jwt/jwa/ecdsa.rb @@ -59,6 +59,10 @@ def verify(data:, signature:, verification_key:) register_algorithm(new(v[:algorithm], v[:digest])) end + def self.from_algorithm(algorithm) + new(algorithm, algorithm.downcase.gsub('es', 'sha')) + end + def self.curve_by_name(name) NAMED_CURVES.fetch(name) do raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported" diff --git a/lib/jwt/jwa/hmac.rb b/lib/jwt/jwa/hmac.rb index f2db8faa..ac95c8c6 100644 --- a/lib/jwt/jwa/hmac.rb +++ b/lib/jwt/jwa/hmac.rb @@ -5,6 +5,10 @@ module JWA class Hmac include JWT::JWA::SigningAlgorithm + def self.from_algorithm(algorithm) + new(algorithm, OpenSSL::Digest.new(algorithm.downcase.gsub('hs', 'sha'))) + end + def initialize(alg, digest) @alg = alg @digest = digest diff --git a/lib/jwt/jwa/hmac_rbnacl.rb b/lib/jwt/jwa/hmac_rbnacl.rb index 15870828..a48fb0d5 100644 --- a/lib/jwt/jwa/hmac_rbnacl.rb +++ b/lib/jwt/jwa/hmac_rbnacl.rb @@ -5,6 +5,10 @@ module JWA class HmacRbNaCl include JWT::JWA::SigningAlgorithm + def self.from_algorithm(algorithm) + new(algorithm, ::RbNaCl::HMAC.const_get(algorithm.upcase.gsub('HS', 'SHA'))) + end + def initialize(alg, hmac) @alg = alg @hmac = hmac diff --git a/lib/jwt/jwa/hmac_rbnacl_fixed.rb b/lib/jwt/jwa/hmac_rbnacl_fixed.rb index 909cc810..f697395d 100644 --- a/lib/jwt/jwa/hmac_rbnacl_fixed.rb +++ b/lib/jwt/jwa/hmac_rbnacl_fixed.rb @@ -5,6 +5,10 @@ module JWA class HmacRbNaClFixed include JWT::JWA::SigningAlgorithm + def self.from_algorithm(algorithm) + new(algorithm, ::RbNaCl::HMAC.const_get(algorithm.upcase.gsub('HS', 'SHA'))) + end + def initialize(alg, hmac) @alg = alg @hmac = hmac diff --git a/lib/jwt/jwa/signing_algorithm.rb b/lib/jwt/jwa/signing_algorithm.rb index d00f1c01..65ae8469 100644 --- a/lib/jwt/jwa/signing_algorithm.rb +++ b/lib/jwt/jwa/signing_algorithm.rb @@ -11,6 +11,7 @@ def register_algorithm(algo) def self.included(klass) klass.extend(ClassMethods) + klass.include(JWT::JWA::Compat) end attr_reader :alg diff --git a/lib/jwt/verify.rb b/lib/jwt/verify.rb new file mode 100644 index 00000000..a086392b --- /dev/null +++ b/lib/jwt/verify.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'jwt/error' + +module JWT + # This class was used internally to verify JWT claims. Planned to be removed in ruby-jwt 3.0 + # @private + class Verify # :nodoc: + DEFAULTS = { leeway: 0 }.freeze + METHODS = %w[verify_aud verify_expiration verify_iat verify_iss verify_jti verify_not_before verify_sub verify_required_claims].freeze + + class << self + METHODS.each do |method_name| + define_method(method_name) do |payload, options| + new(payload, options).send(method_name) + end + end + + def verify_claims(payload, options) + Deprecations.warning('The ::JWT::Verify.verify_claims method is deprecated and will be removed in the next major version of ruby-jwt') + ::JWT::Claims.verify!(payload, options) + end + end + + def initialize(payload, options) + Deprecations.warning('The ::JWT::Verify class is deprecated and will be removed in the next major version of ruby-jwt') + @payload = payload + @options = DEFAULTS.merge(options) + end + + METHODS.each do |method_name| + define_method(method_name) do + ::JWT::Claims.verify!(@payload, @options.merge(method_name => true)) + end + end + end +end diff --git a/spec/jwt/claims_validator_spec.rb b/spec/jwt/claims_validator_spec.rb new file mode 100644 index 00000000..be24bc89 --- /dev/null +++ b/spec/jwt/claims_validator_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +RSpec.describe JWT::ClaimsValidator do + let(:validator) { described_class.new(claims) } + + describe '#validate!' do + subject { validator.validate! } + + shared_examples_for 'a NumericDate claim' do |claim| + context "when #{claim} payload is an integer" do + let(:claims) { { claim => 12_345 } } + + it 'does not raise error' do + expect { subject }.not_to raise_error + end + + context 'and key is a string' do + let(:claims) { { claim.to_s => 43.32 } } + + it 'does not raise error' do + expect { subject }.not_to raise_error + end + end + end + + context "when #{claim} payload is a float" do + let(:claims) { { claim => 43.32 } } + + it 'does not raise error' do + expect { subject }.not_to raise_error + end + end + + context "when #{claim} payload is a string" do + let(:claims) { { claim => '1' } } + + it 'raises error' do + expect { subject }.to raise_error JWT::InvalidPayload + end + + context 'and key is a string' do + let(:claims) { { claim.to_s => '1' } } + + it 'raises error' do + expect { subject }.to raise_error JWT::InvalidPayload + end + end + end + + context "when #{claim} payload is a Time object" do + let(:claims) { { claim => Time.now } } + + it 'raises error' do + expect { subject }.to raise_error JWT::InvalidPayload + end + end + + context "when #{claim} payload is a string" do + let(:claims) { { claim => '1' } } + + it 'raises error' do + expect { subject }.to raise_error JWT::InvalidPayload + end + end + end + + context 'exp claim' do + it_should_behave_like 'a NumericDate claim', :exp + end + + context 'iat claim' do + it_should_behave_like 'a NumericDate claim', :iat + end + + context 'nbf claim' do + it_should_behave_like 'a NumericDate claim', :nbf + end + end +end diff --git a/spec/jwt/jwa/ecdsa_spec.rb b/spec/jwt/jwa/ecdsa_spec.rb index 72d36738..fe52f10e 100644 --- a/spec/jwt/jwa/ecdsa_spec.rb +++ b/spec/jwt/jwa/ecdsa_spec.rb @@ -31,4 +31,12 @@ end end end + + context 'backwards compatibility' do + it 'signs and verifies' do + key = OpenSSL::PKey::EC.generate('prime256v1') + signature = described_class.sign('ES256', 'data', key) + expect(described_class.verify('ES256', key, 'data', signature)).to be(true) + end + end end diff --git a/spec/jwt/jwa/eddsa_spec.rb b/spec/jwt/jwa/eddsa_spec.rb new file mode 100644 index 00000000..f97aed0b --- /dev/null +++ b/spec/jwt/jwa/eddsa_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +RSpec.describe 'JWT::JWA::Eddsa' do + let(:key) { RbNaCl::Signatures::Ed25519::SigningKey.generate } + + before do + skip('Requires the rbnacl gem') unless JWT.rbnacl? + end + + context 'backwards compatibility' do + it 'signs and verifies' do + signature = JWT::JWA::Eddsa.sign('RS256', 'data', key) + expect(JWT::JWA::Eddsa.verify('RS256', key.verify_key, 'data', signature)).to be(true) + end + end +end diff --git a/spec/jwt/jwa/hmac_rbnacl_fixed_spec.rb b/spec/jwt/jwa/hmac_rbnacl_fixed_spec.rb index e095cf15..e2d0b950 100644 --- a/spec/jwt/jwa/hmac_rbnacl_fixed_spec.rb +++ b/spec/jwt/jwa/hmac_rbnacl_fixed_spec.rb @@ -41,4 +41,11 @@ it { is_expected.to be(false) } end end + + context 'backwards compatibility' do + it 'signs and verifies' do + signature = JWT::JWA::HmacRbNaClFixed.sign('HS512256', 'data', 'key') + expect(JWT::JWA::HmacRbNaClFixed.verify('HS512256', 'key', 'data', signature)).to be(true) + end + end end diff --git a/spec/jwt/jwa/hmac_rbnacl_spec.rb b/spec/jwt/jwa/hmac_rbnacl_spec.rb new file mode 100644 index 00000000..dc0528bd --- /dev/null +++ b/spec/jwt/jwa/hmac_rbnacl_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.describe 'JWT::JWA::HmacRbNaCl' do + before do + skip('Requires the rbnacl gem') unless JWT.rbnacl_6_or_greater? + end + context 'backwards compatibility' do + it 'signs and verifies' do + signature = JWT::JWA::HmacRbNaCl.sign('HS512256', 'data', 'key') + expect(JWT::JWA::HmacRbNaCl.verify('HS512256', 'key', 'data', signature)).to be(true) + end + end +end diff --git a/spec/jwt/jwa/hmac_spec.rb b/spec/jwt/jwa/hmac_spec.rb index 20504cde..2d98d227 100644 --- a/spec/jwt/jwa/hmac_spec.rb +++ b/spec/jwt/jwa/hmac_spec.rb @@ -125,4 +125,12 @@ it { is_expected.to be(false) } end end + + context 'backwards compatibility' do + it 'signs and verifies' do + signature = described_class.sign('HS256', 'data', 'key') + expect(signature).to be_a(String) + expect(described_class.verify('HS256', 'key', 'data', signature)).to be(true) + end + end end diff --git a/spec/jwt/jwa/ps_spec.rb b/spec/jwt/jwa/ps_spec.rb index 53b57433..31771e32 100644 --- a/spec/jwt/jwa/ps_spec.rb +++ b/spec/jwt/jwa/ps_spec.rb @@ -69,4 +69,11 @@ end end end + + context 'backwards compatibility' do + it 'signs and verifies' do + signature = described_class.sign('PS256', 'data', rsa_key) + expect(described_class.verify('PS256', rsa_key, 'data', signature)).to be(true) + end + end end diff --git a/spec/jwt/jwa/rsa_spec.rb b/spec/jwt/jwa/rsa_spec.rb index ea73c93d..758bd5d5 100644 --- a/spec/jwt/jwa/rsa_spec.rb +++ b/spec/jwt/jwa/rsa_spec.rb @@ -50,4 +50,11 @@ end end end + + context 'backwards compatibility' do + it 'signs and verifies' do + signature = described_class.sign('RS256', 'data', rsa_key) + expect(described_class.verify('RS256', rsa_key, 'data', signature)).to be(true) + end + end end diff --git a/spec/jwt/jwa_spec.rb b/spec/jwt/jwa_spec.rb new file mode 100644 index 00000000..97afb405 --- /dev/null +++ b/spec/jwt/jwa_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +RSpec.describe JWT::JWA do + describe '.create' do + describe 'Backwards compatibility' do + describe 'create, sign and verify' do + it 'finds an algorithm with old api' do + alg = described_class.create('HS256') + signature = alg.sign(data: 'data', signing_key: 'key') + expect(signature).to be_a(String) + expect(alg.verify(data: 'data', signature: signature, verification_key: 'key')).to be(true) + end + end + end + end +end diff --git a/spec/jwt/jwt_spec.rb b/spec/jwt/jwt_spec.rb index 6c280871..779db969 100644 --- a/spec/jwt/jwt_spec.rb +++ b/spec/jwt/jwt_spec.rb @@ -954,7 +954,7 @@ def verify(*) end it 'emits a deprecation warning' do - expect { token }.to output("[DEPRECATION WARNING] Custom algorithms are required to include JWT::JWA::SigningAlgorithm\n").to_stderr + expect { token }.to output(/.*Custom algorithms are required to include JWT::JWA::SigningAlgorithm.*/).to_stderr expect(JWT.decode(token, 'secret', true, algorithm: custom_algorithm.new)).to eq([payload, { 'alg' => 'custom', 'foo' => 'bar' }]) end end diff --git a/spec/jwt/verify_spec.rb b/spec/jwt/verify_spec.rb new file mode 100644 index 00000000..cb259c05 --- /dev/null +++ b/spec/jwt/verify_spec.rb @@ -0,0 +1,336 @@ +# frozen_string_literal: true + +RSpec.describe JWT::Verify do + let(:base_payload) { { 'user_id' => 'some@user.tld' } } + let(:string_payload) { 'beautyexperts_nbf_iat' } + let(:options) { { leeway: 0 } } + + context '.verify_aud(payload, options)' do + let(:scalar_aud) { 'ruby-jwt-aud' } + let(:array_aud) { %w[ruby-jwt-aud test-aud ruby-ruby-ruby] } + let(:scalar_payload) { base_payload.merge('aud' => scalar_aud) } + let(:array_payload) { base_payload.merge('aud' => array_aud) } + + it 'must raise JWT::InvalidAudError when the singular audience does not match' do + expect do + described_class.verify_aud(scalar_payload, options.merge(aud: 'no-match')) + end.to raise_error JWT::InvalidAudError + end + + it 'must raise JWT::InvalidAudError when the payload has an array and none match the supplied value' do + expect do + described_class.verify_aud(array_payload, options.merge(aud: 'no-match')) + end.to raise_error JWT::InvalidAudError + end + + it 'must allow a matching singular audience to pass' do + described_class.verify_aud(scalar_payload, options.merge(aud: scalar_aud)) + end + + it 'must allow an array with any value matching the one in the options' do + described_class.verify_aud(array_payload, options.merge(aud: array_aud.first)) + end + + it 'must allow an array with any value matching any value in the options array' do + described_class.verify_aud(array_payload, options.merge(aud: array_aud)) + end + + it 'must allow a singular audience payload matching any value in the options array' do + described_class.verify_aud(scalar_payload, options.merge(aud: array_aud)) + end + end + + context '.verify_expiration(payload, options)' do + let(:payload) { base_payload.merge('exp' => (Time.now.to_i - 5)) } + + it 'must raise JWT::ExpiredSignature when the token has expired' do + expect do + described_class.verify_expiration(payload, options) + end.to raise_error JWT::ExpiredSignature + end + + it 'must allow some leeway in the expiration when global leeway is configured' do + described_class.verify_expiration(payload, options.merge(leeway: 10)) + end + + it 'must allow some leeway in the expiration when exp_leeway is configured' do + described_class.verify_expiration(payload, options.merge(exp_leeway: 10)) + end + + it 'must be expired if the exp claim equals the current time' do + payload['exp'] = Time.now.to_i + + expect do + described_class.verify_expiration(payload, options) + end.to raise_error JWT::ExpiredSignature + end + + it 'must not consider string containing exp as expired' do + expect(described_class.verify_expiration(string_payload, options)).to eq(nil) + end + + context 'when leeway is not specified' do + let(:options) { {} } + + it 'used a default leeway of 0' do + expect do + described_class.verify_expiration(payload, options) + end.to raise_error JWT::ExpiredSignature + end + end + end + + context '.verify_iat(payload, options)' do + let(:iat) { Time.now.to_f } + let(:payload) { base_payload.merge('iat' => iat) } + + it 'must allow a valid iat' do + described_class.verify_iat(payload, options) + end + + it 'must ignore configured leeway' do + expect { described_class.verify_iat(payload.merge('iat' => (iat + 60)), options.merge(leeway: 70)) } + .to raise_error(JWT::InvalidIatError) + end + + it 'must properly handle integer times' do + described_class.verify_iat(payload.merge('iat' => Time.now.to_i), options) + end + + it 'must raise JWT::InvalidIatError when the iat value is not Numeric' do + expect do + described_class.verify_iat(payload.merge('iat' => 'not a number'), options) + end.to raise_error JWT::InvalidIatError + end + + it 'must raise JWT::InvalidIatError when the iat value is in the future' do + expect do + described_class.verify_iat(payload.merge('iat' => (iat + 120)), options) + end.to raise_error JWT::InvalidIatError + end + + it 'must not validate if the payload is a string containing iat' do + expect(described_class.verify_iat(string_payload, options)).to eq(nil) + end + end + + context '.verify_iss(payload, options)' do + let(:iss) { 'ruby-jwt-gem' } + let(:payload) { base_payload.merge('iss' => iss) } + + let(:invalid_token) { JWT.encode base_payload, payload[:secret] } + + context 'when iss is a String' do + it 'must raise JWT::InvalidIssuerError when the configured issuer does not match the payload issuer' do + expect do + described_class.verify_iss(payload, options.merge(iss: 'mismatched-issuer')) + end.to raise_error JWT::InvalidIssuerError + end + + it 'must raise JWT::InvalidIssuerError when the payload does not include an issuer' do + expect do + described_class.verify_iss(base_payload, options.merge(iss: iss)) + end.to raise_error(JWT::InvalidIssuerError, /received /) + end + + it 'must allow a matching issuer to pass' do + described_class.verify_iss(payload, options.merge(iss: iss)) + end + end + context 'when iss is an Array' do + it 'must raise JWT::InvalidIssuerError when no matching issuers in array' do + expect do + described_class.verify_iss(payload, options.merge(iss: %w[first second])) + end.to raise_error JWT::InvalidIssuerError + end + + it 'must raise JWT::InvalidIssuerError when the payload does not include an issuer' do + expect do + described_class.verify_iss(base_payload, options.merge(iss: %w[first second])) + end.to raise_error(JWT::InvalidIssuerError, /received /) + end + + it 'must allow an array with matching issuer to pass' do + described_class.verify_iss(payload, options.merge(iss: ['first', iss, 'third'])) + end + end + context 'when iss is a RegExp' do + it 'must raise JWT::InvalidIssuerError when the regular expression does not match' do + expect do + described_class.verify_iss(payload, options.merge(iss: /\A(first|second)\z/)) + end.to raise_error JWT::InvalidIssuerError + end + + it 'must raise JWT::InvalidIssuerError when the payload does not include an issuer' do + expect do + described_class.verify_iss(base_payload, options.merge(iss: /\A(first|second)\z/)) + end.to raise_error(JWT::InvalidIssuerError, /received /) + end + + it 'must allow a regular expression matching the issuer to pass' do + described_class.verify_iss(payload, options.merge(iss: /\A(first|#{iss}|third)\z/)) + end + end + context 'when iss is a Proc' do + it 'must raise JWT::InvalidIssuerError when the proc returns false' do + expect do + described_class.verify_iss(payload, options.merge(iss: ->(iss) { iss && iss.start_with?('first') })) + end.to raise_error JWT::InvalidIssuerError + end + + it 'must raise JWT::InvalidIssuerError when the payload does not include an issuer' do + expect do + described_class.verify_iss(base_payload, options.merge(iss: ->(iss) { iss && iss.start_with?('first') })) + end.to raise_error(JWT::InvalidIssuerError, /received /) + end + + it 'must allow a proc that returns true to pass' do + described_class.verify_iss(payload, options.merge(iss: ->(iss) { iss && iss.start_with?('ruby') })) + end + end + context 'when iss is a Method instance' do + def issuer_start_with_first?(issuer) + issuer&.start_with?('first') + end + + def issuer_start_with_ruby?(issuer) + issuer&.start_with?('ruby') + end + + it 'must raise JWT::InvalidIssuerError when the method returns false' do + expect do + described_class.verify_iss(payload, options.merge(iss: method(:issuer_start_with_first?))) + end.to raise_error JWT::InvalidIssuerError + end + + it 'must raise JWT::InvalidIssuerError when the payload does not include an issuer' do + expect do + described_class.verify_iss(base_payload, options.merge(iss: method(:issuer_start_with_first?))) + end.to raise_error(JWT::InvalidIssuerError, /received /) + end + + it 'must allow a method that returns true to pass' do + described_class.verify_iss(payload, options.merge(iss: method(:issuer_start_with_ruby?))) + end + end + end + + context '.verify_jti(payload, options)' do + let(:payload) { base_payload.merge('jti' => 'some-random-uuid-or-whatever') } + + it 'must allow any jti when the verfy_jti key in the options is truthy but not a proc' do + described_class.verify_jti(payload, options.merge(verify_jti: true)) + end + + it 'must raise JWT::InvalidJtiError when the jti is missing' do + expect do + described_class.verify_jti(base_payload, options) + end.to raise_error JWT::InvalidJtiError, /missing/i + end + + it 'must raise JWT::InvalidJtiError when the jti is an empty string' do + expect do + described_class.verify_jti(base_payload.merge('jti' => ' '), options) + end.to raise_error JWT::InvalidJtiError, /missing/i + end + + it 'must raise JWT::InvalidJtiError when verify_jti proc returns false' do + expect do + described_class.verify_jti(payload, options.merge(verify_jti: ->(_jti) { false })) + end.to raise_error JWT::InvalidJtiError, /invalid/i + end + + it 'true proc should not raise JWT::InvalidJtiError' do + described_class.verify_jti(payload, options.merge(verify_jti: ->(_jti) { true })) + end + + it 'it should not throw arguement error with 2 args' do + expect do + described_class.verify_jti(payload, options.merge(verify_jti: lambda { |_jti, _pl| + true + })) + end.to_not raise_error + end + it 'should have payload as second param in proc' do + described_class.verify_jti(payload, options.merge(verify_jti: lambda { |_jti, pl| + expect(pl).to eq(payload) + })) + end + end + + context '.verify_not_before(payload, options)' do + let(:payload) { base_payload.merge('nbf' => (Time.now.to_i + 5)) } + + it 'must raise JWT::ImmatureSignature when the nbf in the payload is in the future' do + expect do + described_class.verify_not_before(payload, options) + end.to raise_error JWT::ImmatureSignature + end + + it 'must allow some leeway in the token age when global leeway is configured' do + described_class.verify_not_before(payload, options.merge(leeway: 10)) + end + + it 'must allow some leeway in the token age when nbf_leeway is configured' do + described_class.verify_not_before(payload, options.merge(nbf_leeway: 10)) + end + + it 'must not validate if the payload is a string containing iat' do + expect(described_class.verify_not_before(string_payload, options)).to eq(nil) + end + end + + context '.verify_sub(payload, options)' do + let(:sub) { 'ruby jwt subject' } + + it 'must raise JWT::InvalidSubError when the subjects do not match' do + expect do + described_class.verify_sub(base_payload.merge('sub' => 'not-a-match'), options.merge(sub: sub)) + end.to raise_error JWT::InvalidSubError + end + + it 'must allow a matching sub' do + described_class.verify_sub(base_payload.merge('sub' => sub), options.merge(sub: sub)) + end + end + + context '.verify_claims' do + let(:fail_verifications_options) { { iss: 'mismatched-issuer', aud: 'no-match', sub: 'some subject' } } + let(:fail_verifications_payload) do + { + 'exp' => (Time.now.to_i - 50), + 'jti' => ' ', + 'iss' => 'some-issuer', + 'nbf' => (Time.now.to_i + 50), + 'iat' => 'not a number', + 'sub' => 'not-a-match' + } + end + + %w[verify_aud verify_expiration verify_iat verify_iss verify_jti verify_not_before verify_sub].each do |method| + let(:payload) { base_payload.merge(fail_verifications_payload) } + it "must skip verification when #{method} option is set to false" do + described_class.verify_claims(payload, options.merge(method => false)) + end + + it "must raise error when #{method} option is set to true" do + expect do + described_class.verify_claims(payload, options.merge(method => true).merge(fail_verifications_options)) + end.to raise_error JWT::DecodeError + end + end + end + + context '.verify_required_claims(payload, options)' do + it 'must raise JWT::MissingRequiredClaim if a required claim is absent' do + expect do + described_class.verify_required_claims(base_payload, options.merge(required_claims: ['exp'])) + end.to raise_error JWT::MissingRequiredClaim + end + + it 'must verify the claims if all required claims are present' do + payload = base_payload.merge('exp' => (Time.now.to_i + 5), 'custom_claim' => true) + described_class.verify_required_claims(payload, options.merge(required_claims: %w[exp custom_claim])) + end + end +end