diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 5d7a97d6..b053b403 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -126,11 +126,12 @@ Style/MethodCallWithoutArgsParentheses: Exclude: - 'spec/jwt_spec.rb' -# Offense count: 1 +# Offense count: 2 # Configuration parameters: EnforcedStyle. # SupportedStyles: module_function, extend_self Style/ModuleFunction: Exclude: + - 'lib/jwt/algos.rb' - 'lib/jwt/signature.rb' # Offense count: 1 diff --git a/lib/jwt/algos.rb b/lib/jwt/algos.rb new file mode 100644 index 00000000..aa46a60f --- /dev/null +++ b/lib/jwt/algos.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'jwt/algos/hmac' +require 'jwt/algos/eddsa' +require 'jwt/algos/ecdsa' +require 'jwt/algos/rsa' +require 'jwt/algos/ps' +require 'jwt/algos/none' +require 'jwt/algos/unsupported' + +# JWT::Signature module +module JWT + # Signature logic for JWT + module Algos + extend self + + ALGOS = [ + Algos::Hmac, + Algos::Ecdsa, + Algos::Rsa, + Algos::Eddsa, + Algos::Ps, + Algos::None, + Algos::Unsupported + ].freeze + + def find(algorithm) + indexed[algorithm && algorithm.downcase] + end + + private + + def indexed + @indexed ||= begin + fallback = [Algos::Unsupported, nil] + ALGOS.each_with_object(Hash.new(fallback)) do |alg, hash| + alg.const_get(:SUPPORTED).each do |code| + hash[code.downcase] = [alg, code] + end + end + end + end + end +end diff --git a/lib/jwt/algos/none.rb b/lib/jwt/algos/none.rb new file mode 100644 index 00000000..17d15f14 --- /dev/null +++ b/lib/jwt/algos/none.rb @@ -0,0 +1,15 @@ +module JWT + module Algos + module None + module_function + + SUPPORTED = %w[none].freeze + + def sign(*); end + + def verify(*) + true + end + end + end +end diff --git a/lib/jwt/algos/unsupported.rb b/lib/jwt/algos/unsupported.rb index 99ddcb71..179a262e 100644 --- a/lib/jwt/algos/unsupported.rb +++ b/lib/jwt/algos/unsupported.rb @@ -3,14 +3,15 @@ module Algos module Unsupported module_function - SUPPORTED = Object.new.tap { |object| object.define_singleton_method(:include?) { |*| true } } - def verify(*) - raise JWT::VerificationError, 'Algorithm not supported' - end + SUPPORTED = [].freeze def sign(*) raise NotImplementedError, 'Unsupported signing method' end + + def verify(*) + raise JWT::VerificationError, 'Algorithm not supported' + end end end end diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index 9ee144fe..29ff181e 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -43,22 +43,23 @@ def verify_signature end def options_includes_algo_in_header? - allowed_algorithms.include? header['alg'] + allowed_algorithms.any? { |alg| alg.casecmp(header['alg']).zero? } end def allowed_algorithms # Order is very important - first check for string keys, next for symbols - if @options.key?('algorithm') - [@options['algorithm']] + algos = if @options.key?('algorithm') + @options['algorithm'] elsif @options.key?(:algorithm) - [@options[:algorithm]] + @options[:algorithm] elsif @options.key?('algorithms') - @options['algorithms'] || [] + @options['algorithms'] elsif @options.key?(:algorithms) - @options[:algorithms] || [] + @options[:algorithms] else [] end + Array(algos) end def find_key(&keyfinder) diff --git a/lib/jwt/encode.rb b/lib/jwt/encode.rb index 35ddba79..bdeeab2f 100644 --- a/lib/jwt/encode.rb +++ b/lib/jwt/encode.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative './algos' require_relative './claims_validator' # JWT::Encode module @@ -10,10 +11,10 @@ class Encode ALG_KEY = 'alg'.freeze def initialize(options) - @payload = options[:payload] - @key = options[:key] - @algorithm = options[:algorithm] - @headers = options[:headers].each_with_object({}) { |(key, value), headers| headers[key.to_s] = value } + @payload = options[:payload] + @key = options[:key] + _, @algorithm = Algos.find(options[:algorithm]) + @headers = options[:headers].each_with_object({}) { |(key, value), headers| headers[key.to_s] = value } end def segments diff --git a/lib/jwt/signature.rb b/lib/jwt/signature.rb index f4a73282..e490d9eb 100644 --- a/lib/jwt/signature.rb +++ b/lib/jwt/signature.rb @@ -2,12 +2,7 @@ require 'jwt/security_utils' require 'openssl' -require 'jwt/algos/hmac' -require 'jwt/algos/eddsa' -require 'jwt/algos/ecdsa' -require 'jwt/algos/rsa' -require 'jwt/algos/ps' -require 'jwt/algos/unsupported' +require 'jwt/algos' begin require 'rbnacl' rescue LoadError @@ -19,33 +14,21 @@ module JWT # Signature logic for JWT module Signature extend self - ALGOS = [ - Algos::Hmac, - Algos::Ecdsa, - Algos::Rsa, - Algos::Eddsa, - Algos::Ps, - Algos::Unsupported - ].freeze ToSign = Struct.new(:algorithm, :msg, :key) ToVerify = Struct.new(:algorithm, :public_key, :signing_input, :signature) def sign(algorithm, msg, key) - algo = ALGOS.find do |alg| - alg.const_get(:SUPPORTED).include? algorithm - end - algo.sign ToSign.new(algorithm, msg, key) + algo, code = Algos.find(algorithm) + algo.sign ToSign.new(code, msg, key) end def verify(algorithm, key, signing_input, signature) - return true if algorithm == 'none' + return true if algorithm.casecmp('none').zero? raise JWT::DecodeError, 'No verification key available' unless key - algo = ALGOS.find do |alg| - alg.const_get(:SUPPORTED).include? algorithm - end - verified = algo.verify(ToVerify.new(algorithm, key, signing_input, signature)) + algo, code = Algos.find(algorithm) + verified = algo.verify(ToVerify.new(code, key, signing_input, signature)) raise(JWT::VerificationError, 'Signature verification raised') unless verified rescue OpenSSL::PKey::PKeyError raise JWT::VerificationError, 'Signature verification raised' diff --git a/spec/jwt_spec.rb b/spec/jwt_spec.rb index 1ffedc99..a5e5f3d6 100644 --- a/spec/jwt_spec.rb +++ b/spec/jwt_spec.rb @@ -510,4 +510,25 @@ end.not_to raise_error end end + + context 'algorithm case insensitivity' do + let(:payload) { { 'a' => 1, 'b' => 'b' } } + + it 'ignores algorithm casing during encode/decode' do + enc = JWT.encode(payload, '', 'hs256') + expect(JWT.decode(enc, '')).to eq([payload, { 'alg' => 'HS256'}]) + + enc = JWT.encode(payload, data[:rsa_private], 'rs512') + expect(JWT.decode(enc, data[:rsa_public], true, algorithm: 'RS512')).to eq([payload, { 'alg' => 'RS512'}]) + + enc = JWT.encode(payload, data[:rsa_private], 'RS512') + expect(JWT.decode(enc, data[:rsa_public], true, algorithm: 'rs512')).to eq([payload, { 'alg' => 'RS512'}]) + end + + it 'raises error for invalid algorithm' do + expect do + JWT.encode(payload, '', 'xyz') + end.to raise_error(NotImplementedError) + end + end end