Skip to content

Commit 5c03189

Browse files
bellebaumanakinj
authored andcommitted
JWKS: Added Documentation
1 parent b947e17 commit 5c03189

File tree

2 files changed

+97
-57
lines changed

2 files changed

+97
-57
lines changed

README.md

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -569,45 +569,64 @@ end
569569

570570
### JSON Web Key (JWK)
571571

572-
JWK is a JSON structure representing a cryptographic key. Currently only supports RSA, EC and HMAC keys. The `jwks` option can be given as a lambda that evaluates every time a kid is resolved.
572+
JWK is a JSON structure representing a cryptographic key. This gem currently supports RSA, EC and HMAC keys.
573573

574-
If the kid is not found from the given set the loader will be called a second time with the `kid_not_found` option set to `true`. The application can choose to implement some kind of JWK cache invalidation or other mechanism to handle such cases.
574+
To encode a JWT using your JWK:
575575

576576
```ruby
577-
jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), kid: 'optional-kid')
578-
payload = { data: 'data' }
579-
headers = { kid: jwk.kid }
577+
optional_parameters = { kid: 'my-kid', use: 'sig', alg: 'RS512' }
578+
jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), optional_parameters)
580579

581-
token = JWT.encode(payload, jwk.keypair, 'RS512', headers)
580+
# Encoding
581+
payload = { data: 'data' }
582+
token = JWT.encode(payload, jwk.keypair, jwk[:alg], kid: jwk[:kid])
582583

583-
# The jwk loader would fetch the set of JWKs from a trusted source,
584-
# to avoid malicious requests triggering cache invalidations there needs to be some kind of grace time or other logic for determining the validity of the invalidation.
585-
# This example only allows cache invalidations every 5 minutes.
586-
jwk_loader = ->(options) do
587-
if options[:kid_not_found] && @cache_last_update < Time.now.to_i - 300
588-
logger.info("Invalidating JWK cache. #{options[:kid]} not found from previous cache")
589-
@cached_keys = nil
590-
end
591-
@cached_keys ||= begin
592-
@cache_last_update = Time.now.to_i
593-
{ keys: [jwk.export] }
594-
end
595-
end
596-
597-
begin
598-
JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader })
599-
rescue JWT::JWKError
600-
# Handle problems with the provided JWKs
601-
rescue JWT::DecodeError
602-
# Handle other decode related issues e.g. no kid in header, no matching public key found etc.
603-
end
584+
# JSON Web Key Set for advertising your signing keys
585+
jwks_hash = JWT::JWK::Set.new(jwk).export
604586
```
605587

606-
or by passing the JWKs as a simple Hash
588+
To decode a JWT using a trusted entity's JSON Web Key Set (JWKS):
607589

590+
```ruby
591+
jwks = JWT::JWK::Set.new(jwks_hash)
592+
jwks.filter! {|key| key[:use] == 'sig' } # Signing keys only!
593+
algorithms = jwks.map { |key| key[:alg] }.compact.uniq
594+
JWT.decode(token, nil, true, algorithms: algorithms, jwks: jwks)
608595
```
609-
jwks = { keys: [{ ... }] } # keys accepts both of string and symbol
610-
JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks})
596+
597+
598+
The `jwks` option can also be given as a lambda that evaluates every time a kid is resolved.
599+
This can be used to implement caching of remotely fetched JWK Sets.
600+
601+
If the requested `kid` is not found from the given set the loader will be called a second time with the `kid_not_found` option set to `true`.
602+
The application can choose to implement some kind of JWK cache invalidation or other mechanism to handle such cases.
603+
604+
```ruby
605+
jwks_loader = ->(options) do
606+
# The jwk loader would fetch the set of JWKs from a trusted source.
607+
# To avoid malicious requests triggering cache invalidations there needs to be
608+
# some kind of grace time or other logic for determining the validity of the invalidation.
609+
# This example only allows cache invalidations every 5 minutes.
610+
if options[:kid_not_found] && @cache_last_update < Time.now.to_i - 300
611+
logger.info("Invalidating JWK cache. #{options[:kid]} not found from previous cache")
612+
@cached_keys = nil
613+
end
614+
@cached_keys ||= begin
615+
@cache_last_update = Time.now.to_i
616+
# Replace with your own JWKS fetching routine
617+
jwks = JWT::JWK::Set.new(jwks_hash)
618+
jwks.select! { |key| key[:use] == 'sig' } # Signing Keys only
619+
jwks
620+
end
621+
end
622+
623+
begin
624+
JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks_loader })
625+
rescue JWT::JWKError
626+
# Handle problems with the provided JWKs
627+
rescue JWT::DecodeError
628+
# Handle other decode related issues e.g. no kid in header, no matching public key found etc.
629+
end
611630
```
612631

613632
### Importing and exporting JSON Web Keys
@@ -633,11 +652,18 @@ jwk_hash_with_private_key = jwk.export(include_private: true)
633652
# Export as OpenSSL key
634653
public_key = jwk.public_key
635654
private_key = jwk.keypair if jwk.private?
655+
656+
# You can also import and export entire JSON Web Key Sets
657+
jwks_hash = { keys: [{ kty: 'oct', k: 'my-secret', kid: 'my-kid' }] }
658+
jwks = JWT::JWK::Set.new(jwks_hash)
659+
jwks_hash = jwks.export
636660
```
637661

638662
### Key ID (kid) and JWKs
639663

640-
The key id (kid) generation in the gem is a custom algorithm and not based on any standards. To use a standardized JWK thumbprint (RFC 7638) as the kid for JWKs a generator type can be specified in the global configuration or can be given to the JWK instance on initialization.
664+
The key id (kid) generation in the gem is a custom algorithm and not based on any standards.
665+
To use a standardized JWK thumbprint (RFC 7638) as the kid for JWKs a generator type can be specified in the global configuration
666+
or can be given to the JWK instance on initialization.
641667

642668
```ruby
643669
JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint
@@ -649,7 +675,6 @@ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), nil, kid_generator: ::JWT::JWK:
649675
jwk_hash = jwk.export
650676

651677
thumbprint_as_the_kid = jwk_hash[:kid]
652-
653678
```
654679

655680
# Development and Tests

spec/integration/readme_examples_spec.rb

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,28 @@
273273
end.not_to raise_error
274274
end
275275

276-
context 'The JWK loader example' do
276+
context 'The JWK based encode/decode routine' do
277+
it 'works as expected' do
278+
# ---------- ENCODE ----------
279+
optional_parameters = { kid: 'my-kid', use: 'sig', alg: 'RS512' }
280+
jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), optional_parameters)
281+
282+
# Encoding
283+
payload = { data: 'data' }
284+
token = JWT.encode(payload, jwk.keypair, jwk[:alg], kid: jwk[:kid])
285+
286+
# JSON Web Key Set for advertising your signing keys
287+
jwks_hash = JWT::JWK::Set.new(jwk).export
288+
289+
# ---------- DECODE ----------
290+
jwks = JWT::JWK::Set.new(jwks_hash)
291+
jwks.filter! {|key| key[:use] == 'sig' } # Signing keys only!
292+
algorithms = jwks.map { |key| key[:alg] }.compact.uniq
293+
JWT.decode(token, nil, true, algorithms: algorithms, jwks: jwks)
294+
end
295+
end
296+
297+
context 'The JWKS loader example' do
277298
let(:logger_output) { StringIO.new }
278299
let(:logger) { Logger.new(logger_output) }
279300

@@ -324,49 +345,38 @@
324345
end
325346

326347
it 'works as expected' do
327-
jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), kid: 'optional-kid')
348+
jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), use: 'sig')
349+
jwks_hash = JWT::JWK::Set.new(jwk)
328350
payload = { data: 'data' }
329351
headers = { kid: jwk.kid }
330352

331353
token = JWT.encode(payload, jwk.keypair, 'RS512', headers)
332354

333-
# The jwk loader would fetch the set of JWKs from a trusted source,
334-
# to avoid malicious invalidations some kind of protection needs to be implemented.
335-
# This example only allows cache invalidations every 5 minutes.
336-
jwk_loader = ->(options) do
355+
jwks_loader = ->(options) do
356+
# The jwk loader would fetch the set of JWKs from a trusted source.
357+
# To avoid malicious requests triggering cache invalidations there needs to be
358+
# some kind of grace time or other logic for determining the validity of the invalidation.
359+
# This example only allows cache invalidations every 5 minutes.
337360
if options[:kid_not_found] && @cache_last_update < Time.now.to_i - 300
338361
logger.info("Invalidating JWK cache. #{options[:kid]} not found from previous cache")
339362
@cached_keys = nil
340363
end
341364
@cached_keys ||= begin
342365
@cache_last_update = Time.now.to_i
343-
{ keys: [jwk.export] }
366+
# Replace with your own JWKS fetching routine
367+
jwks = JWT::JWK::Set.new(jwks_hash)
368+
jwks.select! { |key| key[:use] == 'sig' } # Signing Keys only
369+
jwks
344370
end
345371
end
346-
372+
347373
begin
348-
JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader })
374+
JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks_loader })
349375
rescue JWT::JWKError
350376
# Handle problems with the provided JWKs
351377
rescue JWT::DecodeError
352378
# Handle other decode related issues e.g. no kid in header, no matching public key found etc.
353379
end
354-
355-
## This is not in the example but verifies that the cache is invalidated after 5 minutes
356-
jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), 'new-kid')
357-
358-
headers = { kid: jwk.kid }
359-
360-
token = JWT.encode(payload, jwk.keypair, 'RS512', headers)
361-
@cache_last_update = Time.now.to_i - 301
362-
363-
JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader })
364-
expect(logger_output.string.chomp).to match(/^I, .* : Invalidating JWK cache. new-kid not found from previous cache/)
365-
366-
jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), 'yet-another-new-kid')
367-
headers = { kid: jwk.kid }
368-
token = JWT.encode(payload, jwk.keypair, 'RS512', headers)
369-
expect { JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader }) }.to raise_error(JWT::DecodeError, 'Could not find public key for kid yet-another-new-kid')
370380
end
371381
end
372382

@@ -386,6 +396,11 @@
386396
# Export as OpenSSL key
387397
_public_key = jwk.public_key
388398
_private_key = jwk.keypair if jwk.private?
399+
400+
# You can also import and export entire JSON Web Key Sets
401+
jwks_hash = { keys: [{ kty: 'oct', k: 'my-secret', kid: 'my-kid' }] }
402+
jwks = JWT::JWK::Set.new(jwks_hash)
403+
_jwks_hash = jwks.export
389404
end
390405

391406
it 'JWK with thumbprint as kid via symbol' do

0 commit comments

Comments
 (0)