Skip to content

Conversation

@ydah
Copy link
Contributor

@ydah ydah commented Jan 29, 2026

Description

Implements duplicate claim name detection as specified in RFC 7519 Section 4, which states:

The Claim Names within a JWT Claims Set MUST be unique; JWT parsers MUST either reject JWTs with duplicate Claim Names or use a JSON parser that returns only the lexically last duplicate member name.

see: https://datatracker.ietf.org/doc/html/rfc7519#section-4

This feature allows users to reject JWTs that contain duplicate keys in the header or payload, which is recommended for security-sensitive applications to prevent claim confusion attacks.

Checklist

Before the PR can be merged be sure the following are checked:

  • There are tests for the fix or feature added/changed
  • A description of the changes and a reference to the PR has been added to CHANGELOG.md. More details in the CONTRIBUTING.md

Implements duplicate claim name detection as specified in RFC 7519 Section 4, which states:

> The Claim Names within a JWT Claims Set MUST be unique; JWT parsers MUST either reject JWTs with duplicate Claim Names or use a JSON parser that returns only the lexically last duplicate member name.

This feature allows users to reject JWTs that contain duplicate keys in the header or payload, which is recommended for security-sensitive applications to prevent claim confusion attacks.
@ydah ydah force-pushed the duplicate_claim_detection branch from 0838041 to bf095f9 Compare January 29, 2026 13:17
lib/jwt/json.rb Outdated

# @api private
# Checks for duplicate keys in a JSON string using a StringScanner-based tokenizer
# rubocop:disable Style/RedundantRegexpArgument
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

# @raise [ArgumentError] if the provided JWT is not a String.
def initialize(jwt)
# @raise [JWT::DuplicateKeyError] if allow_duplicate_keys is false and duplicate keys are found.
def initialize(jwt, allow_duplicate_keys: true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would like to avoid bloating the initializer with feature parameters, the original JWT.encode and JWT.decode methods became parameter monsters over time.

A few random options, not sure if Im a fan of either of my own suggestions :)

token = EncodedToken.new(jwt)
token.raise_on_duplicate_keys!
token = EncodedToken.new(jwt)
token.decode_payload!(raise_on_duplicate_keys:true)

Or allowing changing the default JSON decoder with a stricter one, token.raise_on_duplicate_keys! could internally do something like this.

token = EncodedToken.new(jwt)
token.parser = JWT::JSON.new(raise_on_duplicate_keys:true)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've organized it. What do you think?

lib/jwt/json.rb Outdated
# JWT::JSON.parse('{"a":1,"a":2}', allow_duplicate_keys: false)
# # => raises JWT::DuplicateKeyError
def parse(data, allow_duplicate_keys: true)
DuplicateKeyChecker.check!(data) unless allow_duplicate_keys
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we try to use the built-in-soon-to-be-default ::JSON.parse(data, allow_duplicate_key: false) to deal with this issue. Feels like this gem is not supposed to be dealing with parsing concerns.

Also the new default will kick in in json 3.0. Think if the user has not taken a stance the default behavior of the json gem should be honored.

So adding as little logic as possible related to this feature will make future maintenance easier.

token.header['alg']
end

def allow_duplicate_keys?(options)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have the urge not to add features to the old API anymore. Could we try that and document how to use this feature using the new EncodedToken class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added instructions for use. How does that look?

@anakinj
Copy link
Member

anakinj commented Jan 29, 2026

Overall I support this feature and think it should be the default behavior.

My biggest concern is adding a lot of logic to this now it will stay and live in the gem in the future.

Would almost just like to require json >= 2.13.0 and always parse the json with allow_duplicate_key: false. Then ship a jwt gem with this potentially breaking change. If needed we can then add a feature to loosen this behavior by for example allowing swapping the JSON parser.

@ydah ydah force-pushed the duplicate_claim_detection branch from 1acb9e3 to 11450ac Compare January 30, 2026 10:00
@ydah ydah requested a review from anakinj January 30, 2026 10:01
os:
- ubuntu-latest
ruby:
- "2.5"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEMO: We added json gem >= 2.13.0 to the dependencies, but this version requires Ruby 2.7+. Since ruby-jwt has required_ruby_version = ‘>= 2.5’, dependency resolution fails during bundle install in a Ruby 2.5 environment.

@ydah
Copy link
Contributor Author

ydah commented Jan 30, 2026

I updated this PR. WDYT?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants