-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
67 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# Sometimes you might need a different locking strategy than advisory locks, | ||
# but still use the database-backed storage for idempotent responses. This can arise | ||
# if you are using pgbouncer for instance, where advisory locks are not available | ||
# when using the "transaction mode". You can modify the backend to use a different | ||
# locking mechanism, but keep the rest. | ||
|
||
class ActiveRecordBackendWithDistributedLock < Idempo::ActiveRecordBackend | ||
class LocksViaService | ||
def acquire(_conn, based_on_str) | ||
LockingService.acquire("idempo-lk-#{based_on_str}") | ||
end | ||
|
||
def release(_conn, based_on_str) | ||
LockingService.release("idempo-lk-#{based_on_str}") | ||
true | ||
end | ||
end | ||
|
||
def lock_implementation_for_connection(_connection) | ||
LocksViaService.new | ||
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# Sometimes authentication is done using a Bearer token with a signature, or using another token format | ||
# which includes some form of expiration. This means that every time a request is made, the `Authorization` | ||
# HTTP header may have a different value, and thus the request fingerprint could change every time, | ||
# even though the idempotency key is the same. | ||
# For this case, a custom fingerprinting function can be used. For example, if the bearer token is | ||
# generated in JWT format by the client, it may include the `iss` (issuer) claim, identifying the | ||
# specific device. This identifier can then be used instead of the entire Authorization header. | ||
|
||
module FingerprinterWithIssuerClaim | ||
def self.call(idempotency_key, rack_request) | ||
d = Digest::SHA256.new | ||
d << idempotency_key << "\n" | ||
d << rack_request.url << "\n" | ||
d << rack_request.request_method << "\n" | ||
d << extract_jwt_iss_claim(rack_request) << "\n" | ||
while (chunk = rack_request.env["rack.input"].read(1024 * 65)) | ||
d << chunk | ||
end | ||
Base64.strict_encode64(d.digest) | ||
ensure | ||
rack_request.env["rack.input"].rewind | ||
end | ||
|
||
def self.extract_jwt_iss_claim(rack_request) | ||
header_value = rack_request.get_header("HTTP_AUTHORIZATION").to_s | ||
return header_value unless header_value.start_with?("Bearer ") | ||
|
||
jwt = header_value.delete_prefix("Bearer ") | ||
# This is decoding without verification, but in this case it is reasonably safe | ||
# as we are not actually authenticating the request - just using the `iss` claim. | ||
# It can make the app slightly more sensitive to replay attacks but since the request | ||
# is idempotent, an already executed (and authenticated) request that generated a | ||
# cached response is reasonably safe to serve out. | ||
unverified_claims, _unverified_header = JWT.decode(jwt, _key = nil, _verify = false) | ||
unverified_claims.fetch("iss") | ||
rescue | ||
# If we fail to pick up the claim or anything else - assume the request is non-idempotent | ||
# as treating it otherwise may create a replay attack | ||
SecureRandom.bytes(32) | ||
end | ||
end |