jwk-loader 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf3152f212e3f95a56d2f9c46eacd9ddf4a6330f51689096f89263b811a894c1
4
- data.tar.gz: 44b49a622abb3da44c9a77c3eb6e47050efa0735a43c7fc56ebada54442d8296
3
+ metadata.gz: cc35d8bbe5c77c12e387ee48e54543073527d3851148d6c313cd062e7da5de5d
4
+ data.tar.gz: 9ee9af3ebd760f4608265ba99a845427d5a047c0a5fe1ded70297f6f408a8838
5
5
  SHA512:
6
- metadata.gz: 02253e632ac22d54c64898a03c2cebb8dc568cdd937eadbc8380a20720b631d382deebaf591b8bf830d0dc52f1cb0cfa4e380f5ec1a775911ab0edb2f37982c0
7
- data.tar.gz: 61223d3bb1a49c4b532dedf59cdc173174a54220fd448c8f302cfe001bfe16c3066e77d063473a67da56cc14c0a4680ef40bd25080079ca21ab606f834cab64f
6
+ metadata.gz: 7c1da1b6d607b9006a3b8efe55bac39bf8d61db95d137db6705d9eb08ba5eb3340bb66fbad2b15945c4abdf8550143a75b8d34b80975c159baeb11728808c0e6
7
+ data.tar.gz: 2d14daadf7bf995b2324b3c9ccf46bc2cc67ecc4c00314e20dcc0d01a866dda2056d0b3f5e83a4df00d9f7c0ca02c9154714f2299481c41707a35f80b763cfc6
data/.rubocop.yml CHANGED
@@ -17,6 +17,7 @@ Layout/LineLength:
17
17
  Naming/FileName:
18
18
  Exclude:
19
19
  - 'lib/jwk-loader.rb'
20
+ - 'spec/jwk-loader_spec.rb'
20
21
 
21
22
  Metrics/BlockLength:
22
23
  Exclude:
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.0.0] - 2023-12-28
4
+
5
+ - `jwk_loader/test` for convenience for testing without external dependencies. [#6](https://github.com/anakinj/jwk-loader/pull/6) ([@anakinj](https://github.com/anakinj))
6
+ - Serialize the cached key sets into `JWT::JWK:Set` to avoid generating OpenSSL PKeys for each time the keys are used. [#6](https://github.com/anakinj/jwk-loader/pull/6) ([@anakinj](https://github.com/anakinj))
7
+
8
+ ## [0.1.1] - 2022-08-26
9
+
10
+ - make sure 'net/http' is required [#2](https://github.com/anakinj/jwk-loader/pull/2) ([@lukad](https://github.com/lukad)).
11
+
3
12
  ## [0.1.0] - 2022-07-06
4
13
 
5
14
  - Initial release
data/Gemfile CHANGED
@@ -11,5 +11,3 @@ gem "rubocop", "~> 1.32.0"
11
11
  gem "simplecov"
12
12
  gem "vcr"
13
13
  gem "webmock"
14
-
15
- gem "jwt"
data/README.md CHANGED
@@ -16,16 +16,56 @@ If bundler is not being used to manage dependencies, install the gem by executin
16
16
 
17
17
  ### Using as a jwks loader when decoding JWT tokens
18
18
 
19
- ```
19
+ ```ruby
20
20
  require "jwt"
21
21
  require "jwk-loader"
22
22
 
23
23
  JWT.decode(token, nil, true, algorithm: "RS512", jwks: JwkLoader.for_uri(uri: "https://url/to/public/jwks") )
24
24
  ```
25
25
 
26
+ ### Testing endpoints protected by JWT tokens
27
+
28
+ When testing HTTP endpoints protected by asymmetric JWT keys the mechanism in `jwk_loader/test` can be used to simplify the process.
29
+
30
+ ```ruby
31
+ require 'jwk_loader/test'
32
+
33
+ RSpec.describe 'GET /protected' do
34
+ include JwkLoader::Test
35
+
36
+ context 'when called with a valid token' do
37
+ let(:token) { sign_test_token(token_payload: { user_id: 'user' }, jwk_endpoint: 'https://url/to/public/jwks') }
38
+ subject(:response) { get('/protected', { 'HTTP_AUTHORIZATION' => "Bearer #{token}" }) }
39
+
40
+ it 'is a success' do
41
+ expect(response.status).to eq(200)
42
+ end
43
+ end
44
+ end
45
+ ```
46
+
26
47
  ### Configuring the gem
27
48
 
28
- TODO: Will probably be implemented at some point
49
+ ```ruby
50
+ require "jwt-loader"
51
+
52
+ JwkLoader.configure do |config|
53
+ config[:cache] = YetAnotherCache.new
54
+ config[:cache_grace_period] = 999
55
+ end
56
+ ```
57
+
58
+ or in alternative
59
+
60
+ ```ruby
61
+ require "jwt-loader"
62
+
63
+ JwkLoader.configure do |config|
64
+ config.cache = YetAnotherCache.new
65
+ config.cache_grace_period = 999
66
+ end
67
+ ```
68
+
29
69
 
30
70
  ## Development
31
71
 
@@ -33,7 +73,7 @@ After checking out the repo, run `bundle install` to install dependencies. Then,
33
73
 
34
74
  ## Contributing
35
75
 
36
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/jwk-loader. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/jwk-loader/blob/main/CODE_OF_CONDUCT.md).
76
+ Bug reports and pull requests are welcome on GitHub at https://github.com/anakinj/jwk-loader. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/anakinj/jwk-loader/blob/main/CODE_OF_CONDUCT.md).
37
77
 
38
78
  ## License
39
79
 
data/jwk-loader.gemspec CHANGED
@@ -32,4 +32,5 @@ Gem::Specification.new do |spec|
32
32
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
33
33
  spec.require_paths = ["lib"]
34
34
  spec.add_dependency "concurrent-ruby"
35
+ spec.add_dependency "jwt", "~> 2.6"
35
36
  end
data/lib/jwk-loader.rb CHANGED
@@ -5,9 +5,37 @@ require_relative "jwk_loader/jwks"
5
5
  require_relative "jwk_loader/jwks_uri_provider"
6
6
  require_relative "jwk_loader/memory_cache"
7
7
  require_relative "jwk_loader/error"
8
+ require_relative "jwk_loader/config/config"
8
9
 
9
10
  module JwkLoader
10
- def self.for_uri(**options)
11
- JwksUriProvider.new(**options)
11
+ class << self
12
+ def for_uri(**options)
13
+ options[:cache] ||= config[:cache]
14
+ options[:cache_grace_period] ||= config[:cache_grace_period]
15
+ JwksUriProvider.new(**options)
16
+ end
17
+
18
+ def cache
19
+ config[:cache]
20
+ end
21
+
22
+ def configure
23
+ yield config
24
+ end
25
+
26
+ def config
27
+ @config ||= JwkLoader::Config.new.tap do |cfg|
28
+ cfg[:cache] = MemoryCache.new
29
+ cfg[:cache_grace_period] = 900
30
+ end
31
+ end
32
+
33
+ def reset!
34
+ @config = nil
35
+ end
36
+
37
+ def memory_store
38
+ @memory_store ||= MemoryCache.new
39
+ end
12
40
  end
13
41
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwkLoader
4
+ class Config
5
+ class ConfigurationNotFound < JwkLoader::Error
6
+ def initialize(key)
7
+ super "Configuration for #{key} not available"
8
+ end
9
+ end
10
+
11
+ def []=(key, value)
12
+ registry[key] = value
13
+ end
14
+
15
+ def [](key)
16
+ registry[key] || (raise ConfigurationNotFound, key)
17
+ end
18
+
19
+ def method_missing(name, *args)
20
+ return send(:[]=, name.to_s[0..-2].to_sym, *args) if name.to_s.end_with?("=")
21
+
22
+ send(:[], name, *args)
23
+ end
24
+
25
+ def respond_to_missing?(_name, _include_private)
26
+ true
27
+ end
28
+
29
+ private
30
+
31
+ def registry
32
+ @registry ||= {}
33
+ end
34
+ end
35
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "uri"
4
4
  require "json"
5
+ require "net/http"
5
6
 
6
7
  module JwkLoader
7
8
  module Jwks
@@ -13,6 +14,10 @@ module JwkLoader
13
14
  from_json(response.body)
14
15
  end
15
16
 
17
+ def from_memory(uri)
18
+ JwkLoader.memory_store.fetch(uri)
19
+ end
20
+
16
21
  def from_json(jwks_json)
17
22
  JSON.parse(jwks_json, symbolize_names: true)
18
23
  end
@@ -1,16 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JwkLoader
4
- def self.cache
5
- @cache ||= MemoryCache.new
6
- end
7
-
8
4
  class JwksUriProvider
9
5
  attr_reader :uri, :cache, :cache_grace_period
10
6
 
11
- def initialize(uri:, cache: JwkLoader.cache, cache_grace_period: 900)
7
+ def initialize(uri:, cache:, cache_grace_period:)
12
8
  @uri = uri
13
- @cache = cache || MemoryCache.new
9
+ @cache = cache
14
10
  @cache_grace_period = cache_grace_period
15
11
  end
16
12
 
@@ -22,7 +18,7 @@ module JwkLoader
22
18
  private
23
19
 
24
20
  def jwks
25
- from_cache || from_uri
21
+ from_cache || from_memory || from_uri
26
22
  end
27
23
 
28
24
  def invalidate_cache!
@@ -35,12 +31,17 @@ module JwkLoader
35
31
  cache_entry&.fetch(:jwks)
36
32
  end
37
33
 
34
+ def from_memory
35
+ JwkLoader::Jwks.from_memory(uri)
36
+ end
37
+
38
38
  def cache_entry
39
39
  cache.fetch(uri)
40
40
  end
41
41
 
42
42
  def from_uri
43
- JwkLoader::Jwks.from_uri(uri).tap do |jwks|
43
+ data = JwkLoader::Jwks.from_uri(uri)
44
+ JWT::JWK::Set.new(data).tap do |jwks|
44
45
  cache.store(uri, jwks: jwks, fetched_at: Time.now)
45
46
  end
46
47
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwkLoader
4
+ module Test
5
+ def generate_signing_key(algorithm:)
6
+ case algorithm
7
+ when "RS256", "RS384", "RS512"
8
+ OpenSSL::PKey::RSA.new(2048)
9
+ when "ES256"
10
+ OpenSSL::PKey::EC.generate("prime256v1")
11
+ else
12
+ raise "Unsupported algorithm: #{algorithm}"
13
+ end
14
+ end
15
+
16
+ def test_signing_key_for(jwk_endpoint:, algorithm: "RS512")
17
+ key_set = JwkLoader.memory_store.fetch(jwk_endpoint)
18
+
19
+ if key_set.nil?
20
+ key_set = JWT::JWK::Set.new([generate_signing_key(algorithm: algorithm)])
21
+ JwkLoader.memory_store.store(jwk_endpoint, key_set)
22
+ end
23
+
24
+ key_set.first
25
+ end
26
+
27
+ def sign_test_token(token_payload:, jwk_endpoint:, algorithm: "RS512")
28
+ key = test_signing_key_for(jwk_endpoint: jwk_endpoint, algorithm: algorithm)
29
+ JWT.encode(token_payload, key.signing_key, algorithm, kid: key[:kid])
30
+ end
31
+ end
32
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JwkLoader
4
- VERSION = "0.1.0"
4
+ VERSION = "1.0.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jwk-loader
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joakim Antman
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-08-12 00:00:00.000000000 Z
11
+ date: 2023-12-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: jwt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.6'
27
41
  description:
28
42
  email:
29
43
  - antmanj@gmail.com
@@ -41,10 +55,12 @@ files:
41
55
  - Rakefile
42
56
  - jwk-loader.gemspec
43
57
  - lib/jwk-loader.rb
58
+ - lib/jwk_loader/config/config.rb
44
59
  - lib/jwk_loader/error.rb
45
60
  - lib/jwk_loader/jwks.rb
46
61
  - lib/jwk_loader/jwks_uri_provider.rb
47
62
  - lib/jwk_loader/memory_cache.rb
63
+ - lib/jwk_loader/test.rb
48
64
  - lib/jwk_loader/version.rb
49
65
  homepage: https://github.com/anakinj/jwk-loader
50
66
  licenses:
@@ -53,7 +69,7 @@ metadata:
53
69
  allowed_push_host: https://rubygems.org
54
70
  homepage_uri: https://github.com/anakinj/jwk-loader
55
71
  source_code_uri: https://github.com/anakinj/jwk-loader
56
- changelog_uri: https://github.com/anakinj/jwk-loader/blob/0.1.0/CHANGELOG.md
72
+ changelog_uri: https://github.com/anakinj/jwk-loader/blob/1.0.0/CHANGELOG.md
57
73
  rubygems_mfa_required: 'true'
58
74
  post_install_message:
59
75
  rdoc_options: []