Skip to content

Commit

Permalink
Prevent payload access for unverified tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
anakinj committed Dec 28, 2024
1 parent ec3fe8a commit 1f7dc18
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 46 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**
Expand Down
1 change: 0 additions & 1 deletion lib/jwt/claims.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 0 additions & 29 deletions lib/jwt/claims/verification_methods.rb

This file was deleted.

6 changes: 3 additions & 3 deletions lib/jwt/decode.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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?

Expand Down
61 changes: 56 additions & 5 deletions lib/jwt/encoded_token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -119,6 +145,27 @@ def valid_signature?(algorithm:, key:)
end
end

# Verifies the claims of the token.
# @param options [Array<Symbol>, 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<Symbol>, Hash] the claims to verify.
# @return [Array<Symbol>] 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<Symbol>, 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
Expand Down Expand Up @@ -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
23 changes: 21 additions & 2 deletions lib/jwt/token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -104,6 +102,27 @@ def sign!(algorithm:, key:)
nil
end

# Verifies the claims of the token.
# @param options [Array<Symbol>, 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<Symbol>, Hash] the claims to verify.
# @return [Array<Symbol>] 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<Symbol>, 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.
Expand Down
26 changes: 20 additions & 6 deletions spec/jwt/encoded_token_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,15 +37,29 @@
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

context 'when token is the empty string' do
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
Expand Down

0 comments on commit 1f7dc18

Please sign in to comment.