Skip to content

Commit 71b43ea

Browse files
committed
Remove duplicate key detection from JWT.decode API in favor of EncodedToken API
1 parent bf095f9 commit 71b43ea

8 files changed

Lines changed: 87 additions & 195 deletions

File tree

README.md

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -327,31 +327,24 @@ encoded_token.payload # => {"pay"=>"load"}
327327

328328
## Duplicate Claim Name Detection
329329

330-
RFC 7519 Section 4 specifies that claim names within a JWT Claims Set MUST be unique. By default, ruby-jwt follows ECMAScript 5.1 behavior and uses the last value for duplicate keys. You can enable strict duplicate key detection to reject JWTs with duplicate claim names.
330+
RFC 7519 Section 4 specifies that claim names within a JWT Claims Set MUST be unique. By default, ruby-jwt follows ECMAScript 5.1 behavior and uses the last value for duplicate keys. You can enable strict duplicate key detection to reject JWTs with duplicate claim names using the `EncodedToken` API.
331331

332-
### Rejecting Duplicate Keys
332+
### Using EncodedToken API
333333

334334
```ruby
335-
# Reject JWTs with duplicate keys in header or payload
335+
# Enable strict duplicate key detection
336+
token = JWT::EncodedToken.new(jwt_string)
337+
token.raise_on_duplicate_keys!
338+
336339
begin
337-
JWT.decode(token, secret, true, algorithm: 'HS256', allow_duplicate_keys: false)
340+
token.verify_signature!(algorithm: 'HS256', key: secret)
341+
token.verify_claims!
342+
token.payload
338343
rescue JWT::DuplicateKeyError => e
339344
puts "Duplicate key detected: #{e.message}"
340345
end
341346
```
342347

343-
### Global Configuration
344-
345-
```ruby
346-
# Globally reject duplicate keys
347-
JWT.configure do |config|
348-
config.decode.allow_duplicate_keys = false
349-
end
350-
351-
# Per-decode override
352-
JWT.decode(token, secret, true, algorithm: 'HS256', allow_duplicate_keys: true)
353-
```
354-
355348
This is recommended for security-sensitive applications to prevent attacks that exploit different systems reading different values from the same JWT.
356349

357350
## Claims

lib/jwt/configuration/decode_configuration.rb

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ class DecodeConfiguration
2424
# @return [Array<String>] the list of acceptable algorithms.
2525
# @!attribute [rw] required_claims
2626
# @return [Array<String>] the list of required claims.
27-
# @!attribute [rw] allow_duplicate_keys
28-
# @return [Boolean] whether to allow duplicate keys in JWT header and payload.
2927

3028
attr_accessor :verify_expiration,
3129
:verify_not_before,
@@ -36,8 +34,7 @@ class DecodeConfiguration
3634
:verify_sub,
3735
:leeway,
3836
:algorithms,
39-
:required_claims,
40-
:allow_duplicate_keys
37+
:required_claims
4138

4239
# Initializes a new DecodeConfiguration instance with default settings.
4340
def initialize
@@ -51,7 +48,6 @@ def initialize
5148
@leeway = 0
5249
@algorithms = ['HS256']
5350
@required_claims = []
54-
@allow_duplicate_keys = true
5551
end
5652

5753
# @api private
@@ -66,8 +62,7 @@ def to_h
6662
verify_sub: verify_sub,
6763
leeway: leeway,
6864
algorithms: algorithms,
69-
required_claims: required_claims,
70-
allow_duplicate_keys: allow_duplicate_keys
65+
required_claims: required_claims
7166
}
7267
end
7368
end

lib/jwt/decode.rb

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class Decode
2222
def initialize(jwt, key, verify, options, &keyfinder)
2323
raise JWT::DecodeError, 'Nil JSON web token' unless jwt
2424

25-
@token = EncodedToken.new(jwt, allow_duplicate_keys: allow_duplicate_keys?(options))
25+
@token = EncodedToken.new(jwt)
2626
@key = key
2727
@options = options
2828
@verify = verify
@@ -119,11 +119,5 @@ def none_algorithm?
119119
def alg_in_header
120120
token.header['alg']
121121
end
122-
123-
def allow_duplicate_keys?(options)
124-
return options[:allow_duplicate_keys] if options.key?(:allow_duplicate_keys)
125-
126-
JWT.configuration.decode.allow_duplicate_keys
127-
end
128122
end
129123
end

lib/jwt/encoded_token.rb

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,34 @@ def payload
3939
# Initializes a new EncodedToken instance.
4040
#
4141
# @param jwt [String] the encoded JWT token.
42-
# @param allow_duplicate_keys [Boolean] whether to allow duplicate keys in header/payload (default: true).
4342
# @raise [ArgumentError] if the provided JWT is not a String.
44-
# @raise [JWT::DuplicateKeyError] if allow_duplicate_keys is false and duplicate keys are found.
45-
def initialize(jwt, allow_duplicate_keys: true)
43+
def initialize(jwt)
4644
raise ArgumentError, 'Provided JWT must be a String' unless jwt.is_a?(String)
4745

4846
@jwt = jwt
49-
@allow_duplicate_keys = allow_duplicate_keys
47+
@allow_duplicate_keys = true
5048
@signature_verified = false
5149
@claims_verified = false
5250

5351
@encoded_header, @encoded_payload, @encoded_signature = jwt.split('.')
5452
end
5553

54+
# Enables strict duplicate key detection for this token.
55+
# When called, the token will raise JWT::DuplicateKeyError if duplicate keys
56+
# are found in the header or payload during parsing.
57+
#
58+
# @example
59+
# token = JWT::EncodedToken.new(jwt_string)
60+
# token.raise_on_duplicate_keys!
61+
# token.header # May raise JWT::DuplicateKeyError
62+
#
63+
# @return [self]
64+
# @raise [JWT::DuplicateKeyError] if duplicate keys are found during subsequent parsing.
65+
def raise_on_duplicate_keys!
66+
@allow_duplicate_keys = false
67+
self
68+
end
69+
5670
# Returns the decoded signature of the JWT token.
5771
#
5872
# @return [String] the decoded signature.

lib/jwt/json.rb

Lines changed: 4 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# frozen_string_literal: true
22

33
require 'json'
4-
require 'strscan'
54

65
module JWT
76
# JSON parsing utilities with duplicate key detection support
@@ -29,101 +28,12 @@ def generate(data)
2928
# JWT::JSON.parse('{"a":1,"a":2}', allow_duplicate_keys: false)
3029
# # => raises JWT::DuplicateKeyError
3130
def parse(data, allow_duplicate_keys: true)
32-
DuplicateKeyChecker.check!(data) unless allow_duplicate_keys
33-
::JSON.parse(data)
34-
end
35-
end
36-
37-
# @api private
38-
# Checks for duplicate keys in a JSON string using a StringScanner-based tokenizer
39-
# rubocop:disable Style/RedundantRegexpArgument
40-
class DuplicateKeyChecker
41-
def self.check!(json_str)
42-
new(json_str).check!
43-
end
44-
45-
def initialize(json_str)
46-
@scanner = StringScanner.new(json_str)
47-
@seen_keys_stack = [[]]
48-
@depth = 0
49-
@in_array_stack = [false]
50-
end
51-
52-
def check!
53-
scan_tokens until @scanner.eos?
54-
end
55-
56-
private
57-
58-
def scan_tokens # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
59-
skip_whitespace
60-
return if @scanner.eos?
61-
62-
if @scanner.scan(/\{/)
63-
handle_object_start
64-
elsif @scanner.scan(/\}/)
65-
handle_container_end
66-
elsif @scanner.scan(/\[/)
67-
handle_array_start
68-
elsif @scanner.scan(/\]/)
69-
@depth -= 1
70-
elsif @scanner.scan(/,/) || @scanner.scan(/:/)
71-
# skip comma and colon
72-
elsif @scanner.scan(/"/)
73-
handle_string
74-
elsif @scanner.scan(/-?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/)
75-
# skip number
76-
elsif @scanner.scan(/true|false|null/)
77-
# skip literal
78-
else
79-
@scanner.getch
80-
end
81-
end
82-
83-
def skip_whitespace
84-
@scanner.scan(/\s+/)
85-
end
86-
87-
def handle_object_start
88-
@depth += 1
89-
@seen_keys_stack[@depth] = []
90-
@in_array_stack[@depth] = false
91-
end
92-
93-
def handle_array_start
94-
@depth += 1
95-
@seen_keys_stack[@depth] = []
96-
@in_array_stack[@depth] = true
97-
end
98-
99-
def handle_container_end
100-
@depth -= 1
101-
end
102-
103-
def handle_string
104-
str = scan_string_content
105-
check_if_key(str)
106-
end
107-
108-
def scan_string_content
109-
str = +''
110-
str << (@scanner.getch || '') until @scanner.scan(/"/)
111-
str
112-
end
113-
114-
def check_if_key(str)
115-
return if @in_array_stack[@depth]
116-
117-
pos = @scanner.pos
118-
skip_whitespace
119-
if @scanner.peek(1) == ':'
120-
raise JWT::DuplicateKeyError, "Duplicate key detected: #{str}" if @seen_keys_stack[@depth].include?(str)
31+
::JSON.parse(data, allow_duplicate_key: allow_duplicate_keys)
32+
rescue ::JSON::ParserError => e
33+
raise JWT::DuplicateKeyError, e.message if e.message.include?('duplicate key')
12134

122-
@seen_keys_stack[@depth] << str
123-
end
124-
@scanner.pos = pos
35+
raise
12536
end
12637
end
127-
# rubocop:enable Style/RedundantRegexpArgument
12838
end
12939
end

ruby-jwt.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Gem::Specification.new do |spec|
3232
spec.require_paths = %w[lib]
3333

3434
spec.add_dependency 'base64'
35+
spec.add_dependency 'json', '>= 2.13.0'
3536

3637
spec.add_development_dependency 'appraisal'
3738
spec.add_development_dependency 'bundler'

0 commit comments

Comments
 (0)