Skip to content

Commit

Permalink
Better support for JWK keyfinder and EncodedToken
Browse files Browse the repository at this point in the history
  • Loading branch information
anakinj committed Dec 29, 2024
1 parent 014d4b3 commit 95dc43f
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 1 deletion.
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

0 comments on commit 95dc43f

Please sign in to comment.