Skip to content

Commit 3c6318f

Browse files
bellebaumanakinj
authored andcommitted
First JWKS draft
1 parent bd495eb commit 3c6318f

File tree

8 files changed

+110
-35
lines changed

8 files changed

+110
-35
lines changed

lib/jwt/jwk.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require_relative 'jwk/key_finder'
4+
require_relative 'jwk/set'
45

56
module JWT
67
module JWK

lib/jwt/jwk/ec.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ def initialize(key, params = nil, options = {})
2222
params = { kid: params } if params.is_a?(String)
2323

2424
key_params = case key
25+
when JWT::JWK::EC
26+
key.export(include_private: true)
2527
when OpenSSL::PKey::EC # Accept OpenSSL key as input
2628
@keypair = key # Preserve the object to avoid recreation
2729
parse_ec_key(key)

lib/jwt/jwk/hmac.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ def initialize(key, params = nil, options = {})
1616
params = { kid: params } if params.is_a?(String)
1717

1818
key_params = case key
19+
when JWT::JWK::HMAC
20+
key.export(include_private: true)
1921
when String # Accept String key as input
2022
{ kty: KTY, k: key }
2123
when Hash

lib/jwt/jwk/key_base.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ def []=(key, value)
3333
@parameters[key.to_sym] = value
3434
end
3535

36+
def ==(other)
37+
self[:kid] == other[:kid]
38+
end
39+
40+
def <=>(other)
41+
self[:kid] <=> other[:kid]
42+
end
43+
3644
private
3745

3846
attr_reader :parameters

lib/jwt/jwk/key_finder.rb

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,57 +5,37 @@ module JWK
55
class KeyFinder
66
def initialize(options)
77
jwks_or_loader = options[:jwks]
8-
@jwks = jwks_or_loader if jwks_or_loader.is_a?(Hash)
9-
@jwk_loader = jwks_or_loader if jwks_or_loader.respond_to?(:call)
8+
9+
@jwks_loader = if jwks_or_loader.respond_to?(:call)
10+
jwks_or_loader
11+
else
12+
->(_options) { jwks_or_loader }
13+
end
1014
end
1115

1216
def key_for(kid)
1317
raise ::JWT::DecodeError, 'No key id (kid) found from token headers' unless kid
1418

1519
jwk = resolve_key(kid)
1620

17-
raise ::JWT::DecodeError, 'No keys found in jwks' if jwks_keys.empty?
21+
raise ::JWT::DecodeError, 'No keys found in jwks' unless @jwks.any?
1822
raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk
1923

20-
::JWT::JWK.import(jwk).keypair
24+
jwk.keypair
2125
end
2226

2327
private
2428

2529
def resolve_key(kid)
26-
jwk = find_key(kid)
30+
# First try without invalidation to facilitate application caching
31+
@jwks ||= JWT::JWK::Set.new(@jwks_loader.call(kid: kid))
32+
jwk = @jwks.find { |key| key[:kid] == kid }
2733

2834
return jwk if jwk
2935

30-
if reloadable?
31-
load_keys(invalidate: true, kid_not_found: true, kid: kid) # invalidate for backwards compatibility
32-
return find_key(kid)
33-
end
34-
35-
nil
36-
end
37-
38-
def jwks
39-
return @jwks if @jwks
40-
41-
load_keys
42-
@jwks
43-
end
44-
45-
def load_keys(opts = {})
46-
@jwks = @jwk_loader.call(opts)
47-
end
48-
49-
def jwks_keys
50-
Array(jwks[:keys] || jwks['keys'])
51-
end
52-
53-
def find_key(kid)
54-
jwks_keys.find { |key| (key[:kid] || key['kid']) == kid }
55-
end
56-
57-
def reloadable?
58-
@jwk_loader
36+
# Second try, invalidate for backwards compatibility
37+
@jwks = JWT::JWK::Set.new(@jwks_loader.call(invalidate: true, kid_not_found: true, kid: kid))
38+
@jwks.find { |key| key[:kid] == kid }
5939
end
6040
end
6141
end

lib/jwt/jwk/rsa.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ def initialize(key, params = nil, options = {})
1717
params = { kid: params } if params.is_a?(String)
1818

1919
key_params = case key
20+
when JWT::JWK::RSA
21+
key.export(include_private: true)
2022
when OpenSSL::PKey::RSA # Accept OpenSSL key as input
2123
@keypair = key # Preserve the object to avoid recreation
2224
parse_rsa_key(key)

lib/jwt/jwk/set.rb

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# frozen_string_literal: true
2+
3+
module JWT
4+
module JWK
5+
class Set
6+
include Enumerable
7+
8+
attr_reader :keys
9+
10+
def initialize(jwks)
11+
jwks ||= {}
12+
13+
@keys = case jwks
14+
when JWT::JWK::Set # Simple duplication
15+
jwks.keys
16+
when JWT::JWK::KeyBase # Singleton
17+
[jwks]
18+
when Hash
19+
jwks = jwks.transform_keys(&:to_sym)
20+
[*jwks[:keys]].map { |k| JWT::JWK.new k }
21+
when Array
22+
jwks.map { |k| JWT::JWK.new k }
23+
else
24+
raise ArgumentError, 'Can only create new JWKS from Hash, Array and JWK'
25+
end
26+
end
27+
28+
def export(options = {})
29+
{ keys: @keys.map { |k| k.export(options) } }
30+
end
31+
32+
def each(&block)
33+
@keys.each(&block)
34+
end
35+
36+
def select!(&block)
37+
return @keys.select! unless block
38+
39+
self if @keys.select!(&block)
40+
end
41+
42+
def reject!(&block)
43+
return @keys.reject! unless block
44+
45+
self if @keys.reject!(&block)
46+
end
47+
48+
alias filter! select!
49+
50+
def size
51+
@keys.size
52+
end
53+
54+
alias length size
55+
56+
def merge(enum)
57+
@keys += JWT::JWK::Set.new(enum.collect)
58+
self
59+
end
60+
61+
def union(enum)
62+
dup.merge(enum)
63+
end
64+
65+
def add(key)
66+
@keys << JWT::JWK.new(key)
67+
self
68+
end
69+
70+
def ==(other)
71+
other.is_a?(JWT::JWK::Set) && keys.sort == other.keys.sort
72+
end
73+
74+
# For symbolic manipulation
75+
alias | union
76+
alias + union
77+
alias << add
78+
end
79+
end
80+
end

spec/jwk/decode_with_jwk_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
describe '.decode for JWK usecase' do
55
let(:keypair) { OpenSSL::PKey::RSA.new(2048) }
66
let(:jwk) { JWT::JWK.new(keypair) }
7-
let(:public_jwks) { { keys: [jwk.export, { kid: 'not_the_correct_one' }] } }
7+
let(:public_jwks) { { keys: [jwk.export, { kid: 'not_the_correct_one', kty: 'oct', k: 'secret' }] } }
88
let(:token_payload) { { 'data' => 'something' } }
99
let(:token_headers) { { kid: jwk.kid } }
1010
let(:signed_token) { described_class.encode(token_payload, jwk.keypair, 'RS512', token_headers) }

0 commit comments

Comments
 (0)