Skip to content

Commit 659b038

Browse files
committed
Do not decode payload when b64 header is false
1 parent c8626a6 commit 659b038

File tree

9 files changed

+156
-9
lines changed

9 files changed

+156
-9
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
- JWT::Token and JWT::EncodedToken for signing and verifying tokens [#621](https://github.com/jwt/ruby-jwt/pull/621) ([@anakinj](https://github.com/anakinj))
1010
- Detached payload support for JWT::Token and JWT::EncodedToken [#630](https://github.com/jwt/ruby-jwt/pull/630) ([@anakinj](https://github.com/anakinj))
11+
- Skip decoding payload if b64 header is present and false [#631](https://github.com/jwt/ruby-jwt/pull/631) ([@anakinj](https://github.com/anakinj))
1112
- Your contribution here
1213

1314
**Fixes and enhancements:**

lib/jwt/claims.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# frozen_string_literal: true
22

33
require_relative 'claims/audience'
4+
require_relative 'claims/crit'
5+
require_relative 'claims/decode_verifier'
46
require_relative 'claims/expiration'
57
require_relative 'claims/issued_at'
68
require_relative 'claims/issuer'
@@ -9,9 +11,8 @@
911
require_relative 'claims/numeric'
1012
require_relative 'claims/required'
1113
require_relative 'claims/subject'
12-
require_relative 'claims/decode_verifier'
13-
require_relative 'claims/verifier'
1414
require_relative 'claims/verification_methods'
15+
require_relative 'claims/verifier'
1516

1617
module JWT
1718
# JWT Claim verifications
@@ -27,7 +28,6 @@ module JWT
2728
# sub
2829
# required
2930
# numeric
30-
#
3131
module Claims
3232
# Represents a claim verification error
3333
Error = Struct.new(:message, keyword_init: true)

lib/jwt/claims/crit.rb

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
module JWT
4+
module Claims
5+
# Responsible of validation the crit header
6+
class Crit
7+
# Initializes a new Crit instance.
8+
#
9+
# @param expected_crits [String] the expected crit header values for the JWT token.
10+
def initialize(expected_crits:)
11+
@expected_crits = Array(expected_crits)
12+
end
13+
14+
# Verifies the critical claim ('crit') in the JWT token header.
15+
#
16+
# @param context [Object] the context containing the JWT payload and header.
17+
# @param _args [Hash] additional arguments (not used).
18+
# @raise [JWT::InvalidCritError] if the crit claim is invalid.
19+
# @return [nil]
20+
def verify!(context:, **_args)
21+
raise(JWT::InvalidCritError, 'Crit header missing') unless context.header['crit']
22+
raise(JWT::InvalidCritError, 'Crit header should be an array') unless context.header['crit'].is_a?(Array)
23+
24+
missing = (expected_crits - context.header['crit'])
25+
raise(JWT::InvalidCritError, "Crit header missing expected values: #{missing.join(', ')}") if missing.any?
26+
27+
nil
28+
end
29+
30+
private
31+
32+
attr_reader :expected_crits
33+
end
34+
end
35+
end

lib/jwt/claims/verifier.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module Verifier
1212
jti: ->(options) { Claims::JwtId.new(validator: options[:jti]) },
1313
aud: ->(options) { Claims::Audience.new(expected_audience: options[:aud]) },
1414
sub: ->(options) { Claims::Subject.new(expected_subject: options[:sub]) },
15-
15+
crit: ->(options) { Claims::Crit.new(expected_crits: options[:crit]) },
1616
required: ->(options) { Claims::Required.new(required_claims: options[:required]) },
1717
numeric: ->(*) { Claims::Numeric.new }
1818
}.freeze

lib/jwt/encoded_token.rb

+27-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class EncodedToken
2323
# @param jwt [String] the encoded JWT token.
2424
# @raise [ArgumentError] if the provided JWT is not a String.
2525
def initialize(jwt)
26-
raise ArgumentError 'Provided JWT must be a String' unless jwt.is_a?(String)
26+
raise ArgumentError, 'Provided JWT must be a String' unless jwt.is_a?(String)
2727

2828
@jwt = jwt
2929
@encoded_header, @encoded_payload, @encoded_signature = jwt.split('.')
@@ -57,7 +57,7 @@ def header
5757
#
5858
# @return [Hash] the payload.
5959
def payload
60-
@payload ||= encoded_payload == '' ? raise(JWT::DecodeError, 'Encoded payload is empty') : parse_and_decode(encoded_payload)
60+
@payload ||= decode_payload
6161
end
6262

6363
# Sets or returns the encoded payload of the JWT token.
@@ -85,6 +85,7 @@ def verify_signature!(algorithm:, key: nil, key_finder: nil)
8585
raise ArgumentError, 'Provide either key or key_finder, not both or neither' if key.nil? == key_finder.nil?
8686

8787
key ||= key_finder.call(self)
88+
8889
return if valid_signature?(algorithm: algorithm, key: key)
8990

9091
raise JWT::VerificationError, 'Signature verification failed'
@@ -107,8 +108,31 @@ def valid_signature?(algorithm:, key:)
107108

108109
private
109110

111+
def decode_payload
112+
raise JWT::DecodeError, 'Encoded payload is empty' if encoded_payload == ''
113+
114+
if unecoded_payload?
115+
verify_claims!(crit: ['b64'])
116+
return parse_unencoded(encoded_payload)
117+
end
118+
119+
parse_and_decode(encoded_payload)
120+
end
121+
122+
def unecoded_payload?
123+
header['b64'] == false
124+
end
125+
110126
def parse_and_decode(segment)
111-
JWT::JSON.parse(::JWT::Base64.url_decode(segment))
127+
parse(::JWT::Base64.url_decode(segment))
128+
end
129+
130+
def parse_unencoded(segment)
131+
parse(segment)
132+
end
133+
134+
def parse(segment)
135+
JWT::JSON.parse(segment)
112136
rescue ::JSON::ParserError
113137
raise JWT::DecodeError, 'Invalid segment encoding'
114138
end

lib/jwt/error.rb

+3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ class InvalidAudError < DecodeError; end
3737
# The InvalidSubError class is raised when the JWT subject (sub) claim is invalid.
3838
class InvalidSubError < DecodeError; end
3939

40+
# The InvalidCritError class is raised when the JWT crit header is invalid.
41+
class InvalidCritError < DecodeError; end
42+
4043
# The InvalidJtiError class is raised when the JWT ID (jti) claim is invalid.
4144
class InvalidJtiError < DecodeError; end
4245

spec/jwt/claims/crit_spec.rb

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe JWT::Claims::Crit do
4+
subject(:verify!) { described_class.new(expected_crits: expected_crits).verify!(context: SpecSupport::Token.new(header: header)) }
5+
let(:expected_crits) { [] }
6+
let(:header) { {} }
7+
8+
context 'when header is missing' do
9+
it 'raises JWT::InvalidCritError' do
10+
expect { verify! }.to raise_error(JWT::InvalidCritError, 'Crit header missing')
11+
end
12+
end
13+
14+
context 'when header is not an array' do
15+
let(:header) { { 'crit' => 'not_an_array' } }
16+
17+
it 'raises JWT::InvalidCritError' do
18+
expect { verify! }.to raise_error(JWT::InvalidCritError, 'Crit header should be an array')
19+
end
20+
end
21+
22+
context 'when header is an array and not containing the expected value' do
23+
let(:header) { { 'crit' => %w[crit1] } }
24+
let(:expected_crits) { %w[crit2] }
25+
it 'raises an InvalidCritError' do
26+
expect { verify! }.to raise_error(JWT::InvalidCritError, 'Crit header missing expected values: crit2')
27+
end
28+
end
29+
30+
context 'when header is an array containing exactly the expected values' do
31+
let(:header) { { 'crit' => %w[crit1 crit2] } }
32+
let(:expected_crits) { %w[crit1 crit2] }
33+
it 'does not raise an error' do
34+
expect(verify!).to eq(nil)
35+
end
36+
end
37+
38+
context 'when header is an array containing at least the expected values' do
39+
let(:header) { { 'crit' => %w[crit1 crit2 crit3] } }
40+
let(:expected_crits) { %w[crit1 crit2] }
41+
it 'does not raise an error' do
42+
expect(verify!).to eq(nil)
43+
end
44+
end
45+
end

spec/jwt/encoded_token_spec.rb

+40-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
RSpec.describe JWT::EncodedToken do
44
let(:payload) { { 'pay' => 'load' } }
5-
let(:encoded_token) { JWT.encode(payload, 'secret', 'HS256') }
5+
let(:header) { {} }
6+
let(:encoded_token) { JWT::Token.new(payload: payload, header: header).tap { |t| t.sign!(algorithm: 'HS256', key: 'secret') }.jwt }
67
let(:detached_payload_token) do
78
JWT::Token.new(payload: payload).tap do |t|
89
t.detach_payload!
910
t.sign!(algorithm: 'HS256', key: 'secret')
1011
end
1112
end
13+
1214
subject(:token) { described_class.new(encoded_token) }
1315

1416
describe '#payload' do
@@ -28,6 +30,16 @@
2830
end
2931
end
3032
end
33+
34+
context 'when payload is not encoded and the b64 crit is enabled' do
35+
subject(:token) { described_class.new(encoded_token) }
36+
let(:encoded_token) { 'eyJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..signature' }
37+
before { token.encoded_payload = '{"foo": "bar"}' }
38+
39+
it 'handles the payload encoding' do
40+
expect(token.payload).to eq({ 'foo' => 'bar' })
41+
end
42+
end
3143
end
3244

3345
describe '#header' do
@@ -99,6 +111,17 @@
99111
expect { token.verify_signature!(algorithm: 'HS256', key: 'key', key_finder: 'finder') }.to raise_error(ArgumentError, 'Provide either key or key_finder, not both or neither')
100112
end
101113
end
114+
115+
context 'when payload is not encoded' do
116+
let(:encoded_token) { 'eyJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..A5dxf2s96_n5FLueVuW1Z_vh161FwXZC4YLPff6dmDY' }
117+
before { token.encoded_payload = '$.02' }
118+
119+
let(:key) { Base64.urlsafe_decode64('AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow') }
120+
121+
it 'does not raise' do
122+
expect(token.verify_signature!(algorithm: 'HS256', key: key)).to eq(nil)
123+
end
124+
end
102125
end
103126

104127
describe '#verify_claims!' do
@@ -150,6 +173,22 @@
150173
end
151174
end
152175
end
176+
177+
context 'when header contains crits header' do
178+
let(:header) { { crit: ['b64'] } }
179+
180+
context 'when expected crits are missing' do
181+
it 'raises an error' do
182+
expect { token.verify_claims!(crit: ['other']) }.to raise_error(JWT::InvalidCritError, 'Crit header missing expected values: other')
183+
end
184+
end
185+
186+
context 'when expected crits are present' do
187+
it 'passes verification' do
188+
expect { token.verify_claims!(crit: ['b64']) }.not_to raise_error
189+
end
190+
end
191+
end
153192
end
154193

155194
describe '#valid_claims?' do

spec/spec_support/token.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# frozen_string_literal: true
22

33
module SpecSupport
4-
Token = Struct.new(:payload, keyword_init: true)
4+
Token = Struct.new(:payload, :header, keyword_init: true)
55
end

0 commit comments

Comments
 (0)