Skip to content

Commit

Permalink
Merge pull request #6 from anakinj/test-integration
Browse files Browse the repository at this point in the history
Module for testing and optimizing PKey generation
  • Loading branch information
anakinj authored Dec 28, 2023
2 parents 518b336 + 84d254e commit 6bb7001
Show file tree
Hide file tree
Showing 11 changed files with 112 additions and 15 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
## [Unreleased]

## [1.0.0] - 2023-12-28

- `jwk_loader/test` for convenience for testing without external dependencies. [#6](https://github.com/anakinj/jwk-loader/pull/6) ([@anakinj](https://github.com/anakinj))
- 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))

## [0.1.1] - 2022-08-26

- make sure 'net/http' is required [#1](https://github.com/anakinj/jwk-loader/pull/2) ([@lukad](https://github.com/lukad)).
- make sure 'net/http' is required [#2](https://github.com/anakinj/jwk-loader/pull/2) ([@lukad](https://github.com/lukad)).

## [0.1.0] - 2022-07-06

Expand Down
2 changes: 0 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,3 @@ gem "rubocop", "~> 1.32.0"
gem "simplecov"
gem "vcr"
gem "webmock"

gem "jwt"
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,34 @@ If bundler is not being used to manage dependencies, install the gem by executin

### Using as a jwks loader when decoding JWT tokens

```
```ruby
require "jwt"
require "jwk-loader"

JWT.decode(token, nil, true, algorithm: "RS512", jwks: JwkLoader.for_uri(uri: "https://url/to/public/jwks") )
```

### Testing endpoints protected by JWT tokens

When testing HTTP endpoints protected by asymmetric JWT keys the mechanism in `jwk_loader/test` can be used to simplify the process.

```ruby
require 'jwk_loader/test'

RSpec.describe 'GET /protected' do
include JwkLoader::Test

context 'when called with a valid token' do
let(:token) { sign_test_token(token_payload: { user_id: 'user' }, jwk_endpoint: 'https://url/to/public/jwks') }
subject(:response) { get('/protected', { 'HTTP_AUTHORIZATION' => "Bearer #{token}" }) }

it 'is a success' do
expect(response.status).to eq(200)
end
end
end
```

### Configuring the gem

```ruby
Expand All @@ -45,6 +66,7 @@ JwkLoader.configure do |config|
end
```


## Development

After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
Expand Down
1 change: 1 addition & 0 deletions jwk-loader.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
spec.add_dependency "concurrent-ruby"
spec.add_dependency "jwt", "~> 2.6"
end
14 changes: 12 additions & 2 deletions lib/jwk-loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,27 @@ def for_uri(**options)
JwksUriProvider.new(**options)
end

def cache
config[:cache]
end

def configure
yield config
end

private

def config
@config ||= JwkLoader::Config.new.tap do |cfg|
cfg[:cache] = MemoryCache.new
cfg[:cache_grace_period] = 900
end
end

def reset!
@config = nil
end

def memory_store
@memory_store ||= MemoryCache.new
end
end
end
4 changes: 4 additions & 0 deletions lib/jwk_loader/jwks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ def from_uri(uri)
from_json(response.body)
end

def from_memory(uri)
JwkLoader.memory_store.fetch(uri)
end

def from_json(jwks_json)
JSON.parse(jwks_json, symbolize_names: true)
end
Expand Down
13 changes: 7 additions & 6 deletions lib/jwk_loader/jwks_uri_provider.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
# frozen_string_literal: true

module JwkLoader
def self.cache
@cache ||= MemoryCache.new
end

class JwksUriProvider
attr_reader :uri, :cache, :cache_grace_period

Expand All @@ -22,7 +18,7 @@ def call(options)
private

def jwks
from_cache || from_uri
from_cache || from_memory || from_uri
end

def invalidate_cache!
Expand All @@ -35,12 +31,17 @@ def from_cache
cache_entry&.fetch(:jwks)
end

def from_memory
JwkLoader::Jwks.from_memory(uri)
end

def cache_entry
cache.fetch(uri)
end

def from_uri
JwkLoader::Jwks.from_uri(uri).tap do |jwks|
data = JwkLoader::Jwks.from_uri(uri)
JWT::JWK::Set.new(data).tap do |jwks|
cache.store(uri, jwks: jwks, fetched_at: Time.now)
end
end
Expand Down
32 changes: 32 additions & 0 deletions lib/jwk_loader/test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

module JwkLoader
module Test
def generate_signing_key(algorithm:)
case algorithm
when "RS256", "RS384", "RS512"
OpenSSL::PKey::RSA.new(2048)
when "ES256"
OpenSSL::PKey::EC.generate("prime256v1")
else
raise "Unsupported algorithm: #{algorithm}"
end
end

def test_signing_key_for(jwk_endpoint:, algorithm: "RS512")
key_set = JwkLoader.memory_store.fetch(jwk_endpoint)

if key_set.nil?
key_set = JWT::JWK::Set.new([generate_signing_key(algorithm: algorithm)])
JwkLoader.memory_store.store(jwk_endpoint, key_set)
end

key_set.first
end

def sign_test_token(token_payload:, jwk_endpoint:, algorithm: "RS512")
key = test_signing_key_for(jwk_endpoint: jwk_endpoint, algorithm: algorithm)
JWT.encode(token_payload, key.signing_key, algorithm, kid: key[:kid])
end
end
end
2 changes: 1 addition & 1 deletion lib/jwk_loader/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module JwkLoader
VERSION = "0.1.1"
VERSION = "1.0.0"
end
24 changes: 24 additions & 0 deletions spec/jwk_loader/test_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

require "jwk_loader/test"

RSpec.describe JwkLoader::Test do
subject(:test_context) do
Class.new do
include JwkLoader::Test
end.new
end

describe "#sign_test_token" do
context "when asked to sign a payload" do
it "returns a signed token that can be verified" do
token = test_context.sign_test_token(token_payload: {},
jwk_endpoint: "https://example.com/.well-known/jwks.json",
algorithm: "RS512")
expect(token).to be_a(String)
decoded_token = JWT.decode(token, nil, true, algorithm: "RS512", jwks: JwkLoader.for_uri(uri: "https://example.com/.well-known/jwks.json"))
expect(decoded_token.first).to eq({})
end
end
end
end
4 changes: 2 additions & 2 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
c.syntax = :expect
end

config.before(:each) do
::JwkLoader.cache.clear
config.after(:each) do
::JwkLoader.reset!
end
end

Expand Down

0 comments on commit 6bb7001

Please sign in to comment.