Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent payload access for unverified tokens #648

Merged
merged 1 commit into from
Dec 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading