diff --git a/lib/jwt.rb b/lib/jwt.rb index b78801a9..c6d6d26b 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -4,25 +4,22 @@ require 'jwt/default_options' require 'jwt/encode' require 'jwt/error' -require 'jwt/json' require 'jwt/signature' +require 'jwt/verify' # JSON Web Token implementation # # Should be up to date with the latest spec: # https://tools.ietf.org/html/rfc7519#section-4.1.5 module JWT - extend JWT::Json include JWT::DefaultOptions module_function - def decoded_segments(jwt, key = nil, verify = true, custom_options = {}, &keyfinder) + def decoded_segments(jwt, verify = true) raise(JWT::DecodeError, 'Nil JSON web token') unless jwt - merged_options = DEFAULT_OPTIONS.merge(custom_options) - - decoder = Decode.new jwt, key, verify, merged_options, &keyfinder + decoder = Decode.new jwt, verify decoder.decode_segments end @@ -35,10 +32,12 @@ def decode(jwt, key = nil, verify = true, custom_options = {}, &keyfinder) raise(JWT::DecodeError, 'Nil JSON web token') unless jwt merged_options = DEFAULT_OPTIONS.merge(custom_options) - decoder = Decode.new jwt, key, verify, merged_options, &keyfinder + + decoder = Decode.new jwt, verify header, payload, signature, signing_input = decoder.decode_segments decode_verify_signature(key, header, payload, signature, signing_input, merged_options, &keyfinder) if verify - decoder.verify + + Verify.verify_claims(payload, merged_options) raise(JWT::DecodeError, 'Not enough or too many segments') unless header && payload @@ -65,12 +64,4 @@ def signature_algorithm_and_key(header, payload, key, &keyfinder) end [header['alg'], key] end - - def base64url_decode(str) - Decode.base64url_decode(str) - end - - def base64url_encode(str) - Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '') - end end diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index 0d814244..820828c6 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -1,21 +1,20 @@ # frozen_string_literal: true -require 'jwt/json' -require 'jwt/verify' +require 'json' # JWT::Decode module module JWT - extend JWT::Json - # Decoding logic for JWT class Decode attr_reader :header, :payload, :signature - def initialize(jwt, key, verify, options, &keyfinder) + def self.base64url_decode(str) + str += '=' * (4 - str.length.modulo(4)) + Base64.decode64(str.tr('-_', '+/')) + end + + def initialize(jwt, verify) @jwt = jwt - @key = key @verify = verify - @options = options - @keyfinder = keyfinder end def decode_segments @@ -26,32 +25,21 @@ def decode_segments [@header, @payload, @signature, signing_input] end + private + def raw_segments(jwt, verify) segments = jwt.split('.') required_num_segments = verify ? [3] : [2, 3] raise(JWT::DecodeError, 'Not enough or too many segments') unless required_num_segments.include? segments.length segments end - private :raw_segments def decode_header_and_payload(header_segment, payload_segment) - header = JWT.decode_json(Decode.base64url_decode(header_segment)) - payload = JWT.decode_json(Decode.base64url_decode(payload_segment)) + header = JSON.parse(Decode.base64url_decode(header_segment)) + payload = JSON.parse(Decode.base64url_decode(payload_segment)) [header, payload] - end - private :decode_header_and_payload - - def self.base64url_decode(str) - str += '=' * (4 - str.length.modulo(4)) - Base64.decode64(str.tr('-_', '+/')) - end - - def verify - @options.each do |key, val| - next unless key.to_s =~ /verify/ - - Verify.send(key, payload, @options) if val - end + rescue JSON::ParserError + raise JWT::DecodeError, 'Invalid segment encoding' end end end diff --git a/lib/jwt/encode.rb b/lib/jwt/encode.rb index eb0f21ff..17967844 100644 --- a/lib/jwt/encode.rb +++ b/lib/jwt/encode.rb @@ -1,14 +1,16 @@ # frozen_string_literal: true -require 'jwt/json' +require 'json' # JWT::Encode module module JWT - extend JWT::Json - # Encoding logic for JWT class Encode attr_reader :payload, :key, :algorithm, :header_fields, :segments + def self.base64url_encode(str) + Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '') + end + def initialize(payload, key, algorithm, header_fields) @payload = payload @key = key @@ -21,12 +23,12 @@ def initialize(payload, key, algorithm, header_fields) def encoded_header(algorithm, header_fields) header = { 'alg' => algorithm }.merge(header_fields) - JWT.base64url_encode(JWT.encode_json(header)) + Encode.base64url_encode(JSON.generate(header)) end def encoded_payload(payload) raise InvalidPayload, 'exp claim must be an integer' if payload['exp'] && payload['exp'].is_a?(Time) - JWT.base64url_encode(JWT.encode_json(payload)) + Encode.base64url_encode(JSON.generate(payload)) end def encoded_signature(signing_input, key, algorithm) @@ -34,7 +36,7 @@ def encoded_signature(signing_input, key, algorithm) '' else signature = JWT::Signature.sign(algorithm, signing_input, key) - JWT.base64url_encode(signature) + Encode.base64url_encode(signature) end end diff --git a/lib/jwt/json.rb b/lib/jwt/json.rb deleted file mode 100644 index f33a3bc1..00000000 --- a/lib/jwt/json.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true -require 'json' - -module JWT - # JSON fallback implementation or ruby 1.8.x - module Json - def decode_json(encoded) - JSON.parse(encoded) - rescue JSON::ParserError - raise JWT::DecodeError, 'Invalid segment encoding' - end - - def encode_json(raw) - JSON.generate(raw) - end - end -end diff --git a/lib/jwt/verify.rb b/lib/jwt/verify.rb index 4ba8cd7a..6a738baf 100644 --- a/lib/jwt/verify.rb +++ b/lib/jwt/verify.rb @@ -10,6 +10,13 @@ class << self new(payload, options).send(method_name) end end + + def verify_claims(payload, options) + options.each do |key, val| + next unless key.to_s =~ /verify/ + Verify.send(key, payload, options) if val + end + end end def initialize(payload, options) @@ -18,7 +25,7 @@ def initialize(payload, options) end def verify_aud - return unless (options_aud = extract_option(:aud)) + return unless (options_aud = @options[:aud]) raise(JWT::InvalidAudError, "Invalid audience. Expected #{options_aud}, received #{@payload['aud'] || ''}") if ([*@payload['aud']] & [*options_aud]).empty? end @@ -33,12 +40,12 @@ def verify_iat end def verify_iss - return unless (options_iss = extract_option(:iss)) + return unless (options_iss = @options[:iss]) raise(JWT::InvalidIssuerError, "Invalid issuer. Expected #{options_iss}, received #{@payload['iss'] || ''}") if @payload['iss'].to_s != options_iss.to_s end def verify_jti - options_verify_jti = extract_option(:verify_jti) + options_verify_jti = @options[:verify_jti] if options_verify_jti.respond_to?(:call) raise(JWT::InvalidJtiError, 'Invalid jti') unless options_verify_jti.call(@payload['jti']) elsif @payload['jti'].to_s.strip.empty? @@ -52,30 +59,26 @@ def verify_not_before end def verify_sub - return unless (options_sub = extract_option(:sub)) + return unless (options_sub = @options[:sub]) raise(JWT::InvalidSubError, "Invalid subject. Expected #{options_sub}, received #{@payload['sub'] || ''}") unless @payload['sub'].to_s == options_sub.to_s end private - def extract_option(key) - @options.values_at(key.to_sym, key.to_s).compact.first - end - def global_leeway - extract_option :leeway + @options[:leeway] end def exp_leeway - extract_option(:exp_leeway) || global_leeway + @options[:exp_leeway] || global_leeway end def iat_leeway - extract_option(:iat_leeway) || global_leeway + @options[:iat_leeway] || global_leeway end def nbf_leeway - extract_option(:nbf_leeway) || global_leeway + @options[:nbf_leeway] || global_leeway end end end diff --git a/ruby-jwt.gemspec b/ruby-jwt.gemspec index 644409e0..dcd4fa27 100644 --- a/ruby-jwt.gemspec +++ b/ruby-jwt.gemspec @@ -22,7 +22,6 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'bundler' spec.add_development_dependency 'rake' - spec.add_development_dependency 'json' spec.add_development_dependency 'rspec' spec.add_development_dependency 'simplecov' spec.add_development_dependency 'simplecov-json' diff --git a/spec/jwt/verify_spec.rb b/spec/jwt/verify_spec.rb index 2e055b74..46f5898d 100644 --- a/spec/jwt/verify_spec.rb +++ b/spec/jwt/verify_spec.rb @@ -25,56 +25,21 @@ module JWT end.to raise_error JWT::InvalidAudError end - it 'must raise JWT::InvalidAudError when the singular audience does not match and the options aud key is a string' do - expect do - Verify.verify_aud(scalar_payload, options.merge('aud' => 'no-match')) - end.to raise_error JWT::InvalidAudError - end - it 'must allow a matching singular audience to pass' do Verify.verify_aud(scalar_payload, options.merge(aud: scalar_aud)) end - it 'must allow a matching audence to pass when the options key is a string' do - Verify.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 Verify.verify_aud(array_payload, options.merge(aud: array_aud.first)) end - it 'must allow an array with any value matching the one in the options with a string options key' do - Verify.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 Verify.verify_aud(array_payload, options.merge(aud: array_aud)) end - it 'must allow an array with any value matching any value in the options array with a string options key' do - Verify.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 Verify.verify_aud(scalar_payload, options.merge(aud: array_aud)) end - - it 'must allow a singular audience payload matching any value in the options array with a string options key' do - Verify.verify_aud(scalar_payload, options.merge('aud' => array_aud)) - end - - it 'should allow strings or symbols in options array' do - options['aud'] = [ - 'ruby-jwt-aud', - 'test-aud', - 'ruby-ruby-ruby', - :test - ] - - array_payload['aud'].push('test') - - Verify.verify_aud(array_payload, options) - end end context '.verify_expiration(payload, options)' do diff --git a/spec/jwt_spec.rb b/spec/jwt_spec.rb index c7930c8f..b379ea01 100644 --- a/spec/jwt_spec.rb +++ b/spec/jwt_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' require 'jwt' +require 'jwt/encode' require 'jwt/decode' describe JWT do @@ -225,7 +226,7 @@ context 'Base64' do it 'urlsafe replace + / with - _' do allow(Base64).to receive(:encode64) { 'string+with/non+url-safe/characters_' } - expect(JWT.base64url_encode('foo')).to eq('string-with_non-url-safe_characters_') + expect(JWT::Encode.base64url_encode('foo')).to eq('string-with_non-url-safe_characters_') end end end