Skip to content
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 @@ -15,6 +15,7 @@
- Redact private JWK material from inspection [#754](https://github.com/jwt/ruby-jwt/pull/754) - [@OskarEichler](https://github.com/OskarEichler)
- Reject tokens with too many segments in `JWT::EncodedToken` signature verification [#782](https://github.com/jwt/ruby-jwt/pull/782) ([@anakinj](https://github.com/anakinj))
- Raise `JWT::MalformedTokenError` for a token header that is not a JSON object, including a string header in `JWT.decode` without verification [#783](https://github.com/jwt/ruby-jwt/pull/783) ([@anakinj](https://github.com/anakinj))
- Do not call the `key_finder` of `JWT::EncodedToken` when the token's algorithm is not allowed, so a mismatched `alg` header no longer verifies with a key finder [#784](https://github.com/jwt/ruby-jwt/pull/784) ([@anakinj](https://github.com/anakinj))
- Your contribution here

## [v3.3.0](https://github.com/jwt/ruby-jwt/tree/v3.3.0) (2026-09-11)
Expand Down
12 changes: 12 additions & 0 deletions lib/jwt/encoded_token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,16 @@ def verify_signature!(algorithm:, key: nil, key_finder: nil)
# @param algorithm [String, Array<String>, Object, Array<Object>] the algorithm(s) to use for verification.
# @param key [String, Array<String>, JWT::JWK::KeyBase, Array<JWT::JWK::KeyBase>] the key(s) to use for verification.
# @param key_finder [#call] an object responding to `call` to find the key for verification.
# Not called if the token's algorithm is not among the given algorithms, in which case the
# signature is not valid. The token's alg header is not compared when a key is given directly.
# @return [Boolean] true if the signature is valid, false otherwise.
# @raise [JWT::MalformedTokenError] if the token is structurally invalid.
def valid_signature?(algorithm: nil, key: nil, key_finder: nil)
raise ArgumentError, 'Provide either key or key_finder, not both or neither' if key.nil? == key_finder.nil?

@signature_verified = false
validate_segment_count!
return false if key_finder && !algorithm_allowed?(algorithm)

keys = Array(key || key_finder.call(self))
verifiers = JWA.create_verifiers(algorithms: algorithm, keys: keys, preferred_algorithm: header['alg'])
Expand Down Expand Up @@ -204,6 +208,14 @@ def validate_segment_count!
raise JWT::MalformedTokenError, 'Too many segments' if jwt.count('.') > 2
end

# An empty list leaves the choice of algorithm to the JWKs given by the key finder.
# An unknown algorithm is a configuration error that the verifier reports.
def algorithm_allowed?(algorithms)
algorithms = Array(algorithms).map { |algorithm| JWA.resolve(algorithm) }

algorithms.empty? || algorithms.any? { |algorithm| algorithm == JWA::Unsupported || algorithm.valid_alg?(header['alg']) }
end

def claims_options(options)
return DEFAULT_CLAIMS if options.first.nil?

Expand Down
90 changes: 90 additions & 0 deletions spec/jwt/encoded_token_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,96 @@
it 'can utilize an array provided by keyfinder' do
expect(token.verify_signature!(algorithm: 'HS256', key_finder: ->(_token) { %w[wrong secret] })).to eq(nil)
end

it 'accepts the token when one of several algorithms is allowed' do
expect(token.verify_signature!(algorithm: %w[RS256 HS256], key_finder: ->(_token) { 'secret' })).to eq(nil)
end

it 'accepts algorithm names given as symbols and in a different case' do
expect(token.verify_signature!(algorithm: :HS256, key_finder: ->(_token) { 'secret' })).to eq(nil)
expect(token.verify_signature!(algorithm: 'hs256', key_finder: ->(_token) { 'secret' })).to eq(nil)
end

it 'accepts a custom algorithm object' do
custom = Class.new do
include JWT::JWA::SigningAlgorithm

def alg
'CUSTOM'
end

def sign(*)
'signature'
end

def verify(*)
true
end
end.new
custom_token = JWT::Token.new(payload: payload).tap { |t| t.sign!(algorithm: custom, key: 'secret') }.jwt

expect(described_class.new(custom_token).verify_signature!(algorithm: custom, key_finder: ->(_token) { 'secret' })).to eq(nil)
end

it 'reports an unknown algorithm as a configuration error' do
expect { token.verify_signature!(algorithm: 'FOO', key_finder: ->(_token) { 'secret' }) }.to raise_error(JWT::VerificationKeyError, 'Algorithm not supported')
end

it 'does not load the JWKS through the JWK key finder' do
loader = double('jwks loader')
expect(loader).not_to receive(:call)

expect { token.verify_signature!(algorithm: 'RS256', key_finder: JWT::JWK::KeyFinder.new(jwks: loader)) }.to raise_error(JWT::VerificationError, 'Signature verification failed')
end

context 'when the token algorithm is not allowed' do
let(:key_finder) { ->(_token) { raise 'key finder should not be called' } }

it 'raises without calling the key finder' do
expect { token.verify_signature!(algorithm: 'RS256', key_finder: key_finder) }.to raise_error(JWT::VerificationError, 'Signature verification failed')
end

it 'is reported as invalid without calling the key finder' do
expect(token.valid_signature?(algorithm: %w[RS256 RS512], key_finder: key_finder)).to eq(false)
expect(token.valid?(signature: { algorithm: 'RS256', key_finder: key_finder })).to eq(false)
end

it 'clears an earlier successful verification' do
token.verify_signature!(algorithm: 'HS256', key: 'secret')
token.verify_claims!
expect(token.payload).to eq(payload)

expect(token.valid_signature?(algorithm: 'RS256', key_finder: key_finder)).to eq(false)
expect { token.payload }.to raise_error(JWT::TokenError, 'Verify the token signature before accessing the payload')
end
end

[{}, { 'alg' => nil }, { 'alg' => [] }, { 'alg' => 123 }, { 'alg' => true }, { 'alg' => {} }].each do |invalid_header|
context "when the token header is #{invalid_header}" do
let(:encoded_token) do
encoded = JWT::Base64.url_encode(JSON.generate(invalid_header))
"#{encoded}.#{JWT::Base64.url_encode('{}')}.signature"
end

it 'raises without calling the key finder' do
key_finder = ->(_token) { raise 'key finder should not be called' }
expect { token.verify_signature!(algorithm: 'RS256', key_finder: key_finder) }.to raise_error(JWT::VerificationError, 'Signature verification failed')
end
end
end

context 'when no algorithm is given and the key finder provides a JWK' do
let(:jwk) { JWT::JWK.new(test_pkey('rsa-2048-private.pem'), alg: 'RS256') }
let(:encoded_token) do
JWT::Token.new(payload: payload)
.tap { |t| t.sign!(algorithm: 'RS256', key: jwk.signing_key) }
.jwt
end

it 'uses the JWK for verification' do
expect(token.verify_signature!(algorithm: [], key_finder: ->(_token) { jwk })).to eq(nil)
end
end
end

context 'when neither key or key_finder is given' do
Expand Down
Loading