diff --git a/lib/jwt.rb b/lib/jwt.rb index 8ba45b15..75435472 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -23,6 +23,7 @@ require_relative 'jwt/validators/noop' require_relative 'jwt/validators/claims_validator' require_relative 'jwt/validators/numeric_claims_validator' +require_relative 'jwt/validators/not_before_claim_validator' require_relative 'jwt/decoders/base64_json' module JWT diff --git a/lib/jwt/decode_context.rb b/lib/jwt/decode_context.rb index 6b50c630..be8d0164 100644 --- a/lib/jwt/decode_context.rb +++ b/lib/jwt/decode_context.rb @@ -2,12 +2,13 @@ module JWT class DecodeContext - attr_reader :token, :allowed_algorithms, :verification_key + attr_reader :token, :allowed_algorithms, :verification_key, :validators - def initialize(token:, decoder:, allowed_algorithms:, verification_key:) + def initialize(token:, decoder:, allowed_algorithms:, verification_key:, validators:) @token = Token.new(value: token, decoder: decoder) @allowed_algorithms = allowed_algorithms @verification_key = verification_key + @validators = validators end def header @@ -30,6 +31,10 @@ def algorithm_match? !allowed_and_valid_algorithms.empty? end + def validate! + validators.each { |validator| validator.validate!(payload: payload, header: header) } + end + private def resolve_verification_keys diff --git a/lib/jwt/default_decoder.rb b/lib/jwt/default_decoder.rb index 5f06470c..08167b52 100644 --- a/lib/jwt/default_decoder.rb +++ b/lib/jwt/default_decoder.rb @@ -4,7 +4,7 @@ module JWT class DefaultDecoder - def self.define_decoder(options) + def self.define_decoder(options) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize JWT.define do allowed_algorithms(*options[:allowed_algorithms]) @@ -28,6 +28,10 @@ def self.define_decoder(options) X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c']) end end + + if options[:verify_not_before] + validators << Validators::NotBeforeClaimValidator.new(leeway: options[:nbf_leeway] || options[:leeway]) + end end end @@ -49,6 +53,7 @@ def decode_segments if @verify verify_algo verify_signature + decode_context.validate! verify_claims end diff --git a/lib/jwt/dsl/decoding.rb b/lib/jwt/dsl/decoding.rb index fa560b90..2a6ea644 100644 --- a/lib/jwt/dsl/decoding.rb +++ b/lib/jwt/dsl/decoding.rb @@ -24,11 +24,16 @@ def decoding_validator(value = nil) @decoding_validator || Validators::Noop end + def validators + @validators ||= [] + end + def decode(token:, **options) DecodeContext.new(**{ token: token, decoder: decoder, allowed_algorithms: allowed_algorithms, - verification_key: verification_key }.merge(options)) + verification_key: verification_key, + validators: validators }.merge(options)) end end end diff --git a/lib/jwt/validators/claims_validator.rb b/lib/jwt/validators/claims_validator.rb index 80470c98..9286a338 100644 --- a/lib/jwt/validators/claims_validator.rb +++ b/lib/jwt/validators/claims_validator.rb @@ -8,7 +8,7 @@ class ClaimsValidator }.freeze class << self - %w[verify_aud verify_expiration verify_iat verify_iss verify_jti verify_not_before verify_sub verify_required_claims].each do |method_name| + %w[verify_aud verify_expiration verify_iat verify_iss verify_jti verify_sub verify_required_claims].each do |method_name| define_method method_name do |payload, options| new(payload, options).send(method_name) end @@ -16,7 +16,7 @@ class << self def verify_claims(payload, options) options.each do |key, val| - next unless key.to_s =~ /verify/ + next unless key.to_s =~ /verify/ && respond_to?(key) send(key, payload, options) if val end @@ -74,11 +74,6 @@ def verify_jti end end - def verify_not_before - return unless @payload.include?('nbf') - raise(JWT::ImmatureSignature, 'Signature nbf has not been reached') if @payload['nbf'].to_i > (Time.now.to_i + nbf_leeway) - end - def verify_sub return unless (options_sub = @options[:sub]) @@ -103,10 +98,6 @@ def global_leeway def exp_leeway @options[:exp_leeway] || global_leeway end - - def nbf_leeway - @options[:nbf_leeway] || global_leeway - end end end end diff --git a/lib/jwt/validators/not_before_claim_validator.rb b/lib/jwt/validators/not_before_claim_validator.rb new file mode 100644 index 00000000..ee838510 --- /dev/null +++ b/lib/jwt/validators/not_before_claim_validator.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module JWT + module Validators + class NotBeforeClaimValidator + def initialize(leeway:) + @leeway = leeway + end + + def validate!(payload:, **_args) + return unless payload.is_a?(Hash) + return unless payload.key?('nbf') + + raise JWT::ImmatureSignature, 'Signature nbf has not been reached' if payload['nbf'].to_i > (Time.now.to_i + leeway) + end + + private + + attr_reader :leeway + end + end +end diff --git a/spec/jwt/validators/claims_validator_spec.rb b/spec/jwt/validators/claims_validator_spec.rb index 671e1eac..f8a4a437 100644 --- a/spec/jwt/validators/claims_validator_spec.rb +++ b/spec/jwt/validators/claims_validator_spec.rb @@ -249,24 +249,6 @@ def issuer_start_with_ruby?(issuer) 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 - end - context '.verify_sub(payload, options)' do let(:sub) { 'ruby jwt subject' } @@ -294,7 +276,7 @@ def issuer_start_with_ruby?(issuer) } } - %w[verify_aud verify_expiration verify_iat verify_iss verify_jti verify_not_before verify_sub].each do |method| + %w[verify_aud verify_expiration verify_iat verify_iss verify_jti 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)) diff --git a/spec/jwt/validators/not_before_claim_validator_spec.rb b/spec/jwt/validators/not_before_claim_validator_spec.rb new file mode 100644 index 00000000..b322f813 --- /dev/null +++ b/spec/jwt/validators/not_before_claim_validator_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +RSpec.describe ::JWT::Validators::NotBeforeClaimValidator do + let(:payload) { { 'nbf' => (Time.now.to_i + 5) } } + + describe '#validate!' do + context 'when nbf is in the future' do + it 'raises JWT::ImmatureSignature' do + expect { described_class.new(leeway: 0).validate!(payload: payload) }.to raise_error JWT::ImmatureSignature + end + end + + context 'when nbf is in the past' do + let(:payload) { { 'nbf' => (Time.now.to_i - 5) } } + + it 'does not raise error' do + expect { described_class.new(leeway: 0).validate!(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!(payload: payload) }.not_to raise_error + end + end + end +end