Skip to content

Commit 90398f1

Browse files
committed
Support signing with jwk
1 parent 234ca7d commit 90398f1

9 files changed

Lines changed: 81 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
- Raise an error if the ECDSA signing or verification key is not an instance of `OpenSSL::PKey::EC` [#688](https://github.com/jwt/ruby-jwt/pull/688) ([@anakinj](https://github.com/anakinj))
1111
- Allow `OpenSSL::PKey::EC::Point` to be used as the verification key in ECDSA [#689](https://github.com/jwt/ruby-jwt/pull/689) ([@anakinj](https://github.com/anakinj))
1212
- Require claims to have been verified before accessing the `JWT::EncodedToken#payload` [#690](https://github.com/jwt/ruby-jwt/pull/690) ([@anakinj](https://github.com/anakinj))
13-
- Support verifying token signature using a JWK [#692](https://github.com/jwt/ruby-jwt/pull/692) ([@anakinj](https://github.com/anakinj))
13+
- Support verifying and signing token signature using a JWK [#692](https://github.com/jwt/ruby-jwt/pull/692) ([@anakinj](https://github.com/anakinj))
1414
- Your contribution here
1515

1616
**Fixes and enhancements:**

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ encoded_token.payload # => { 'exp'=>1234, 'jti'=>'1234", 'sub'=>'my-subject' }
251251
encoded_token.header # {'kid'=>'hmac', 'alg'=>'HS256'}
252252
```
253253

254-
A JWK can be used to verify the token if it's possible to derive the signing algorithm from the key.
254+
A JWK can be used to sign and verify the token if it's possible to derive the signing algorithm from the key.
255255

256256
```ruby
257257
jwk_json = '{
@@ -262,6 +262,11 @@ jwk_json = '{
262262
}'
263263

264264
jwk = JWT::JWK.import(JSON.parse(jwk_json))
265+
266+
token = JWT::Token.new(payload: payload, header: header)
267+
268+
token.sign!(key: jwk)
269+
265270
encoded_token = JWT::EncodedToken.new(token.jwt)
266271
encoded_token.verify!(signature: { key: jwk})
267272
```

lib/jwt/encoded_token.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def verify_signature!(algorithm: nil, key: nil, key_finder: nil)
147147
# Checks if the signature of the JWT token is valid.
148148
#
149149
# @param algorithm [String, Array<String>, Object, Array<Object>] the algorithm(s) to use for verification.
150-
# @param key [String, Array<String>] the key(s) to use for verification.
150+
# @param key [String, Array<String>, JWT::JWK::KeyBase, Array<JWT::JWK::KeyBase>] the key(s) to use for verification.
151151
# @param key_finder [#call] an object responding to `call` to find the key for verification.
152152
# @return [Boolean] true if the signature is valid, false otherwise.
153153
def valid_signature?(algorithm: nil, key: nil, key_finder: nil)

lib/jwt/jwa.rb

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,34 +13,60 @@
1313
module JWT
1414
# The JWA module contains all supported algorithms.
1515
module JWA
16+
# @api private
17+
class VerifierContext
18+
def initialize(jwa:, keys:)
19+
@jwa = jwa
20+
@keys = Array(keys)
21+
end
22+
23+
def verify(*args, **kwargs)
24+
@keys.any? do |key|
25+
@jwa.verify(*args, **kwargs, verification_key: key)
26+
end
27+
end
28+
end
29+
30+
# @api private
31+
class SignerContext
32+
def initialize(jwa:, key:)
33+
@jwa = jwa
34+
@key = key
35+
end
36+
37+
def sign(*args, **kwargs)
38+
@jwa.sign(*args, **kwargs, signing_key: @key)
39+
end
40+
41+
def jwa_header
42+
@jwa.header
43+
end
44+
end
45+
1646
class << self
1747
# @api private
1848
def resolve(algorithm)
1949
return find(algorithm) if algorithm.is_a?(String) || algorithm.is_a?(Symbol)
2050

51+
raise ArgumentError, 'Algorithm must be provided' if algorithm.nil?
52+
2153
raise ArgumentError, 'Custom algorithms are required to include JWT::JWA::SigningAlgorithm' unless algorithm.is_a?(SigningAlgorithm)
2254

2355
algorithm
2456
end
2557

2658
# @api private
2759
def resolve_and_sort(algorithms:, preferred_algorithm:)
28-
algs = Array(algorithms).map { |alg| JWA.resolve(alg) }
29-
algs.partition { |alg| alg.valid_alg?(preferred_algorithm) }.flatten
60+
Array(algorithms).map { |alg| JWA.resolve(alg) }
61+
.partition { |alg| alg.valid_alg?(preferred_algorithm) }
62+
.flatten
3063
end
3164

3265
# @api private
33-
class VerificationContext
34-
def initialize(jwa:, keys:)
35-
@jwa = jwa
36-
@keys = Array(keys)
37-
end
66+
def create_signer(algorithm:, key:)
67+
return key if key.is_a?(JWK::KeyBase)
3868

39-
def verify(*args, **kwargs)
40-
@keys.any? do |key|
41-
@jwa.verify(*args, **kwargs, verification_key: key)
42-
end
43-
end
69+
SignerContext.new(jwa: resolve(algorithm), key: key)
4470
end
4571

4672
# @api private
@@ -49,7 +75,7 @@ def create_verifiers(algorithms:, keys:, preferred_algorithm:)
4975

5076
jwks + resolve_and_sort(algorithms: algorithms,
5177
preferred_algorithm: preferred_algorithm)
52-
.map { |jwa| VerificationContext.new(jwa: jwa, keys: other_keys) }
78+
.map { |jwa| VerifierContext.new(jwa: jwa, keys: other_keys) }
5379
end
5480
end
5581
end

lib/jwt/jwk/ec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def []=(key, value)
7373

7474
private
7575

76-
def resolve_algorithm
76+
def jwa
7777
return super if self[:alg]
7878

7979
curve_name = self.class.to_openssl_curve(self[:crv])

lib/jwt/jwk/key_base.rb

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,16 @@ def ==(other)
4343
end
4444

4545
def verify(**kwargs)
46-
resolve_algorithm.verify(**kwargs, verification_key: verify_key)
46+
jwa.verify(**kwargs, verification_key: verify_key)
4747
end
4848

4949
def sign(**kwargs)
50-
resolve_algorithm.sign(**kwargs, signing_key: signing_key)
50+
jwa.sign(**kwargs, signing_key: signing_key)
51+
end
52+
53+
# @api private
54+
def jwa_header
55+
jwa.header
5156
end
5257

5358
alias eql? ==
@@ -60,7 +65,7 @@ def <=>(other)
6065

6166
private
6267

63-
def resolve_algorithm
68+
def jwa
6469
raise JWT::JWKError, 'Could not resolve the JWA, the "alg" parameter is missing' unless self[:alg]
6570

6671
JWA.resolve(self[:alg])

lib/jwt/token.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,16 @@ def detach_payload!
8787

8888
# Signs the JWT token.
8989
#
90+
# @param key [String, JWT::JWK::KeyBase] the key to use for signing.
9091
# @param algorithm [String, Object] the algorithm to use for signing.
91-
# @param key [String] the key to use for signing.
9292
# @return [void]
9393
# @raise [JWT::EncodeError] if the token is already signed or other problems when signing
94-
def sign!(algorithm:, key:)
94+
def sign!(key:, algorithm: nil)
9595
raise ::JWT::EncodeError, 'Token already signed' if @signature
9696

97-
JWA.resolve(algorithm).tap do |algo|
98-
header.merge!(algo.header) { |_key, old, _new| old }
99-
@signature = algo.sign(data: signing_input, signing_key: key)
97+
JWA.create_signer(algorithm: algorithm, key: key).tap do |signer|
98+
header.merge!(signer.jwa_header) { |_key, old, _new| old }
99+
@signature = signer.sign(data: signing_input)
100100
end
101101

102102
nil

spec/integration/readme_examples_spec.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -462,9 +462,6 @@ def self.verify(data:, signature:, verification_key:)
462462
payload = { exp: Time.now.to_i + 60, jti: '1234', sub: 'my-subject' }
463463
header = { kid: 'hmac' }
464464

465-
token = JWT::Token.new(payload: payload, header: header)
466-
token.sign!(algorithm: 'HS256', key: 'secret')
467-
468465
jwk_json = '{
469466
"kty": "oct",
470467
"k": "c2VjcmV0",
@@ -473,6 +470,10 @@ def self.verify(data:, signature:, verification_key:)
473470
}'
474471

475472
jwk = JWT::JWK.import(JSON.parse(jwk_json))
473+
474+
token = JWT::Token.new(payload: payload, header: header)
475+
token.sign!(key: jwk)
476+
476477
encoded_token = JWT::EncodedToken.new(token.jwt)
477478
expect { encoded_token.verify!(signature: { key: jwk }) }.not_to raise_error
478479
end

spec/jwt/token_spec.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,22 @@
2222
expect { token.sign!(algorithm: 'HS256', key: 'secret') }.to raise_error(JWT::EncodeError)
2323
end
2424
end
25+
26+
context 'when JWK is given as key' do
27+
let(:jwk) { JWT::JWK::RSA.new(OpenSSL::PKey::RSA.new(2048), alg: 'RS256') }
28+
29+
it 'signs the token' do
30+
token.sign!(key: jwk)
31+
32+
expect(JWT::EncodedToken.new(token.jwt).valid_signature?(algorithm: 'RS256', key: jwk.verify_key)).to be(true)
33+
end
34+
end
35+
36+
context 'when string key is given but not algorithm' do
37+
it 'raises an error' do
38+
expect { token.sign!(key: 'secret') }.to raise_error(ArgumentError, 'Algorithm must be provided')
39+
end
40+
end
2541
end
2642

2743
describe '#jwt' do

0 commit comments

Comments
 (0)