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

Better support for keyfinders and EncodedToken #663

Merged
merged 1 commit into from
Dec 29, 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 @@ -19,6 +19,7 @@ Take a look at the [upgrade guide](UPGRADING.md) for more details.
**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))
- Do not override the alg header if already given [#659](https://github.com/jwt/ruby-jwt/pull/659) ([@anakinj](https://github.com/anakinj))
- Make `JWK::KeyFinder` compatible with `JWT::EncodedToken` [#663](https://github.com/jwt/ruby-jwt/pull/663) ([@anakinj](https://github.com/anakinj))
- Your contribution here

**Fixes and enhancements:**
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,35 @@ encoded_token.payload # => { 'exp'=>1234, 'jti'=>'1234", 'sub'=>'my-subject' }
encoded_token.header # {'kid'=>'hmac', 'alg'=>'HS256'}
```

A keyfinder can be used to verify a signature. A keyfinder is an object responding to the `#call` method. The method expects to receive one argument, which is the token to be verified.

An example on using the built-in JWK keyfinder:
```ruby
# Create and sign a token
jwk = JWT::JWK.new(OpenSSL::PKey::RSA.generate(2048))
token = JWT::Token.new(payload: { pay: 'load' }, header: { kid: jwk.kid })
token.sign!(algorithm: 'RS256', key: jwk.signing_key)

# Create keyfinder object, verify and decode token
key_finder = JWT::JWK::KeyFinder.new(jwks: JWT::JWK::Set.new(jwk))
encoded_token = JWT::EncodedToken.new(token.jwt)
encoded_token.verify!(signature: { algorithm: 'RS256', key_finder: key_finder})
encoded_token.payload # => { 'pay' => 'load' }
```

Using a custom keyfinder proc:
```ruby
# Create and sign a token
key = OpenSSL::PKey::RSA.generate(2048)
token = JWT::Token.new(payload: { pay: 'load' })
token.sign!(algorithm: 'RS256', key: key)

# Verify and decode token
encoded_token = JWT::EncodedToken.new(token.jwt)
encoded_token.verify!(signature: { algorithm: 'RS256', key_finder: ->(_token){ key.public_key }})
encoded_token.payload # => { 'pay' => 'load' }
```

### Detached payload

The `::JWT::Token#detach_payload!` method can be use to detach the payload from the JWT.
Expand Down
15 changes: 14 additions & 1 deletion lib/jwt/jwk/key_finder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

module JWT
module JWK
# @api private
# JSON Web Key keyfinder
# To find the key for a given kid
class KeyFinder
# Initializes a new KeyFinder instance.
# @param [Hash] options the options to create a KeyFinder with
# @option options [Proc, JWT::JWK::Set] :jwks the jwks or a loader proc
# @option options [Boolean] :allow_nil_kid whether to allow nil kid
def initialize(options)
@allow_nil_kid = options[:allow_nil_kid]
jwks_or_loader = options[:jwks]
Expand All @@ -15,6 +20,8 @@ def initialize(options)
end
end

# Returns the verification key for the given kid
# @param [String] kid the key id
def key_for(kid)
raise ::JWT::DecodeError, 'No key id (kid) found from token headers' unless kid || @allow_nil_kid
raise ::JWT::DecodeError, 'Invalid type for kid header parameter' unless kid.nil? || kid.is_a?(String)
Expand All @@ -27,6 +34,12 @@ def key_for(kid)
jwk.verify_key
end

# Returns the key for the given token
# @param [JWT::EncodedToken] token the token
def call(token)
key_for(token.header['kid'])
end

private

def resolve_key(kid)
Expand Down
14 changes: 14 additions & 0 deletions spec/jwt/encoded_token_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,20 @@
expect(token.verify_signature!(algorithm: 'HS256', key: key)).to eq(nil)
end
end

context 'when JWT::KeyFinder is used as a key_finder' do
let(:jwk) { JWT::JWK.new(test_pkey('rsa-2048-private.pem')) }
let(:encoded_token) do
JWT::Token.new(payload: payload, header: { kid: jwk.kid })
.tap { |t| t.sign!(algorithm: 'RS256', key: jwk.signing_key) }
.jwt
end

it 'uses the keys provided by the JWK key finder' do
key_finder = JWT::JWK::KeyFinder.new(jwks: JWT::JWK::Set.new(jwk))
expect(token.verify_signature!(algorithm: 'RS256', key_finder: key_finder)).to eq(nil)
end
end
end

describe '#verify_claims!' do
Expand Down
Loading