diff --git a/CHANGELOG.md b/CHANGELOG.md index f9bc7541..d0914f08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ **Features:** - JWT::EncodedToken#verify! method that bundles signature and claim validation [#647](https://github.com/jwt/ruby-jwt/pull/647) ([@anakinj](https://github.com/anakinj)) +- Require token signature to be verified before accessing payload (Breaking change!) [#648](https://github.com/jwt/ruby-jwt/pull/648) ([@anakinj](https://github.com/anakinj)) - Your contribution here **Fixes and enhancements:** diff --git a/lib/jwt/claims.rb b/lib/jwt/claims.rb index 581facf3..22492a7d 100644 --- a/lib/jwt/claims.rb +++ b/lib/jwt/claims.rb @@ -11,7 +11,6 @@ require_relative 'claims/numeric' require_relative 'claims/required' require_relative 'claims/subject' -require_relative 'claims/verification_methods' require_relative 'claims/verifier' module JWT diff --git a/lib/jwt/claims/verification_methods.rb b/lib/jwt/claims/verification_methods.rb deleted file mode 100644 index 4c8ec0a8..00000000 --- a/lib/jwt/claims/verification_methods.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -module JWT - module Claims - # Provides methods to verify the claims of a token. - module VerificationMethods - # Verifies the claims of the token. - # @param options [Array, Hash] the claims to verify. - # @raise [JWT::DecodeError] if the claims are invalid. - def verify_claims!(*options) - Verifier.verify!(self, *options) - end - - # Returns the errors of the claims of the token. - # @param options [Array, Hash] the claims to verify. - # @return [Array] the errors of the claims. - def claim_errors(*options) - Verifier.errors(self, *options) - end - - # Returns whether the claims of the token are valid. - # @param options [Array, Hash] the claims to verify. - # @return [Boolean] whether the claims are valid. - def valid_claims?(*options) - claim_errors(*options).empty? - end - end - end -end diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index 743b6b43..67437c03 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -33,10 +33,10 @@ def decode_segments verify_algo set_key verify_signature - Claims::DecodeVerifier.verify!(token.payload, @options) + Claims::DecodeVerifier.verify!(token.unverified_payload, @options) end - [token.payload, token.header] + [token.unverified_payload, token.header] end private @@ -93,7 +93,7 @@ def resolve_allowed_algorithms end def find_key(&keyfinder) - key = (keyfinder.arity == 2 ? yield(token.header, token.payload) : yield(token.header)) + key = (keyfinder.arity == 2 ? yield(token.header, token.unverified_payload) : yield(token.header)) # key can be of type [string, nil, OpenSSL::PKey, Array] return key if key && !Array(key).empty? diff --git a/lib/jwt/encoded_token.rb b/lib/jwt/encoded_token.rb index ac608f3d..d31d9b6a 100644 --- a/lib/jwt/encoded_token.rb +++ b/lib/jwt/encoded_token.rb @@ -12,7 +12,21 @@ module JWT # encoded_token.verify_signature!(algorithm: 'HS256', key: 'secret') # encoded_token.payload # => {'pay' => 'load'} class EncodedToken - include Claims::VerificationMethods + # @private + # Allow access to the unverified payload for claim verification. + class ClaimsContext + extend Forwardable + + def_delegators :@token, :header, :unverified_payload + + def initialize(token) + @token = token + end + + def payload + unverified_payload + end + end # Returns the original token provided to the class. # @return [String] The JWT token. @@ -26,6 +40,7 @@ def initialize(jwt) raise ArgumentError, 'Provided JWT must be a String' unless jwt.is_a?(String) @jwt = jwt + @signature_verified = false @encoded_header, @encoded_payload, @encoded_signature = jwt.split('.') end @@ -53,11 +68,20 @@ def header # @return [String] the encoded header. attr_reader :encoded_header - # Returns the payload of the JWT token. + # Returns the payload of the JWT token. Access requires the signature to have been verified. # # @return [Hash] the payload. + # @raise [JWT::DecodeError] if the signature has not been verified. def payload - @payload ||= decode_payload + raise JWT::DecodeError, 'Verify the token signature before accessing the payload' unless @signature_verified + + decoded_payload + end + + # Returns the payload of the JWT token without requiring the signature to have been verified. + # @return [Hash] the payload. + def unverified_payload + decoded_payload end # Sets or returns the encoded payload of the JWT token. @@ -101,8 +125,10 @@ def verify_signature!(algorithm:, key: nil, key_finder: nil) key ||= key_finder.call(self) - return if valid_signature?(algorithm: algorithm, key: key) - + if valid_signature?(algorithm: algorithm, key: key) + @signature_verified = true + return + end raise JWT::VerificationError, 'Signature verification failed' end @@ -119,6 +145,27 @@ def valid_signature?(algorithm:, key:) end end + # Verifies the claims of the token. + # @param options [Array, Hash] the claims to verify. + # @raise [JWT::DecodeError] if the claims are invalid. + def verify_claims!(*options) + Claims::Verifier.verify!(ClaimsContext.new(self), *options) + end + + # Returns the errors of the claims of the token. + # @param options [Array, Hash] the claims to verify. + # @return [Array] the errors of the claims. + def claim_errors(*options) + Claims::Verifier.errors(ClaimsContext.new(self), *options) + end + + # Returns whether the claims of the token are valid. + # @param options [Array, Hash] the claims to verify. + # @return [Boolean] whether the claims are valid. + def valid_claims?(*options) + claim_errors(*options).empty? + end + alias to_s jwt private @@ -151,5 +198,9 @@ def parse(segment) rescue ::JSON::ParserError raise JWT::DecodeError, 'Invalid segment encoding' end + + def decoded_payload + @decoded_payload ||= decode_payload + end end end diff --git a/lib/jwt/token.rb b/lib/jwt/token.rb index 8a7fb2dc..4f61c839 100644 --- a/lib/jwt/token.rb +++ b/lib/jwt/token.rb @@ -15,8 +15,6 @@ module JWT # token.header # => {"custom"=>"value", "alg"=>"HS256"} # class Token - include Claims::VerificationMethods - # Initializes a new Token instance. # # @param header [Hash] the header of the JWT token. @@ -104,6 +102,27 @@ def sign!(algorithm:, key:) nil end + # Verifies the claims of the token. + # @param options [Array, Hash] the claims to verify. + # @raise [JWT::DecodeError] if the claims are invalid. + def verify_claims!(*options) + Claims::Verifier.verify!(self, *options) + end + + # Returns the errors of the claims of the token. + # @param options [Array, Hash] the claims to verify. + # @return [Array] the errors of the claims. + def claim_errors(*options) + Claims::Verifier.errors(self, *options) + end + + # Returns whether the claims of the token are valid. + # @param options [Array, Hash] the claims to verify. + # @return [Boolean] whether the claims are valid. + def valid_claims?(*options) + claim_errors(*options).empty? + end + # Returns the JWT token as a string. # # @return [String] the JWT token as a string. diff --git a/spec/jwt/encoded_token_spec.rb b/spec/jwt/encoded_token_spec.rb index 037a8127..54ec9fea 100644 --- a/spec/jwt/encoded_token_spec.rb +++ b/spec/jwt/encoded_token_spec.rb @@ -13,20 +13,20 @@ subject(:token) { described_class.new(encoded_token) } - describe '#payload' do - it { expect(token.payload).to eq(payload) } + describe '#unverified_payload' do + it { expect(token.unverified_payload).to eq(payload) } context 'when payload is detached' do let(:encoded_token) { detached_payload_token.jwt } context 'when payload provided in separate' do before { token.encoded_payload = detached_payload_token.encoded_payload } - it { expect(token.payload).to eq(payload) } + it { expect(token.unverified_payload).to eq(payload) } end context 'when payload is not provided' do it 'raises decode error' do - expect { token.payload }.to raise_error(JWT::DecodeError, 'Encoded payload is empty') + expect { token.unverified_payload }.to raise_error(JWT::DecodeError, 'Encoded payload is empty') end end end @@ -37,7 +37,7 @@ before { token.encoded_payload = '{"foo": "bar"}' } it 'handles the payload encoding' do - expect(token.payload).to eq({ 'foo' => 'bar' }) + expect(token.unverified_payload).to eq({ 'foo' => 'bar' }) end end @@ -45,7 +45,21 @@ let(:encoded_token) { '' } it 'raises decode error' do - expect { token.payload }.to raise_error(JWT::DecodeError, 'Invalid segment encoding') + expect { token.unverified_payload }.to raise_error(JWT::DecodeError, 'Invalid segment encoding') + end + end + end + + describe '#payload' do + context 'when token is verified' do + before { token.verify_signature!(algorithm: 'HS256', key: 'secret') } + + it { expect(token.payload).to eq(payload) } + end + + context 'when token is not verified' do + it 'raises an error' do + expect { token.payload }.to raise_error(JWT::DecodeError, 'Verify the token signature before accessing the payload') end end end