-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: Make JWKS cache shared across SDK instances
After 1123911 the JWKs cache wasn't working as expected, since the middleware constructed a new SDK instance on each request. So each request was effectively bypassing the cache, since the cache was essentially an instance variable which was re-initialized anew every time. With this change, the JWKs cache is made thread-safe and shared between all instances of the SDK. Fixes AUTH-76
- Loading branch information
Showing
5 changed files
with
68 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
class JWKSCache | ||
def initialize(lifetime) | ||
@lifetime = lifetime | ||
@jwks = nil | ||
@last_update = nil | ||
@lock = Concurrent::ReadWriteLock.new | ||
end | ||
|
||
def fetch(sdk, force_refresh: false, kid_not_found: false) | ||
should_refresh = @lock.with_read_lock do | ||
@jwks.nil? || @last_update.nil? || force_refresh || | ||
(Time.now.to_i-@last_update > @lifetime) || | ||
(kid_not_found && Time.now.to_i-@last_update > 300) | ||
end | ||
|
||
if should_refresh | ||
@lock.with_write_lock do | ||
@last_update = Time.now.to_i | ||
|
||
@jwks = begin | ||
sdk.jwks.all["keys"] | ||
rescue Clerk::Errors::Base | ||
nil | ||
end | ||
end | ||
end | ||
|
||
@lock.with_read_lock do | ||
@jwks | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,51 +63,51 @@ def test_verify_token_jwks_cache | |
sdk = ::Clerk::SDK.new(connection: conn) | ||
|
||
Timecop.freeze(TIME_WHEN_JWT_IS_VALID) do | ||
sdk.verify_token(json_fixture("jwt_valid")) | ||
assert_equal jwks_endpoint_hits, 1 | ||
sdk.verify_token(json_fixture("jwt_valid"), force_refresh_jwks: true) | ||
assert_equal 1, jwks_endpoint_hits | ||
|
||
sdk.verify_token(json_fixture("jwt_valid")) | ||
assert_equal jwks_endpoint_hits, 1 | ||
assert_equal 1, jwks_endpoint_hits | ||
|
||
sdk.verify_token(json_fixture("jwt_valid"), force_refresh_jwks: true) | ||
assert_equal jwks_endpoint_hits, 2 | ||
assert_equal 2, jwks_endpoint_hits | ||
|
||
sdk.verify_token(json_fixture("jwt_valid")) | ||
assert_equal jwks_endpoint_hits, 2 | ||
assert_equal 2, jwks_endpoint_hits | ||
end | ||
|
||
# cache expired | ||
Timecop.freeze(TIME_WHEN_JWT_IS_VALID+Clerk::SDK::JWKS_CACHE_LIFETIME+1) do | ||
begin | ||
sdk.verify_token(json_fixture("jwt_valid")) | ||
rescue JWT::ExpiredSignature | ||
# we know the token is going to be expired in this travelled to time | ||
end | ||
|
||
assert_equal jwks_endpoint_hits, 3 | ||
assert_equal 3, jwks_endpoint_hits | ||
end | ||
|
||
|
||
# assert that if the last server API request to the JWKS endpoint returned | ||
# an error, then we're skipping the cache in the next request | ||
# assert that if the server (BAPI) returns an error to the JWKS request | ||
# (i.e. BAPI was down momentarily) and therefore the received token couldn't | ||
# be verified, then we're going to try one more time | ||
jwks_endpoint_hits = 0 | ||
conn = Faraday.new do |faraday| | ||
faraday.adapter :test do |stub| | ||
stub.get("/jwks") do | ||
jwks_endpoint_hits += 1 | ||
json_404 | ||
jwks_endpoint_hits < 2 ? json_404 : json_ok("jwks") | ||
end | ||
end | ||
end | ||
|
||
sdk = ::Clerk::SDK.new(connection: conn) | ||
Timecop.freeze(TIME_WHEN_JWT_IS_VALID) do | ||
begin | ||
sdk.verify_token(json_fixture("jwt_valid")) rescue Clerk::Errors::Fatal | ||
sdk.verify_token(json_fixture("jwt_valid")) rescue Clerk::Errors::Fatal | ||
sdk.verify_token(json_fixture("jwt_valid")) rescue Clerk::Errors::Fatal | ||
end | ||
claims = sdk.verify_token(json_fixture("jwt_valid"), force_refresh_jwks: true) | ||
assert_equal 2, jwks_endpoint_hits | ||
assert_equal "[email protected]", claims["email"] | ||
|
||
assert_equal 3, jwks_endpoint_hits | ||
sdk.verify_token(json_fixture("jwt_valid")) | ||
assert_equal 2, jwks_endpoint_hits # cached response | ||
end | ||
end | ||
end |