Skip to content

Commit

Permalink
Add a few more advanced examples
Browse files Browse the repository at this point in the history
  • Loading branch information
julik committed Feb 21, 2024
1 parent 5cb3fea commit a02c35e
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 0 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ Or install it yourself as:

$ gem install idempo

## More advanced use cases

Check out the files in the `examples/` directory to see a few customisations you can do.

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
Expand Down
22 changes: 22 additions & 0 deletions examples/custom_locking.rb
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
41 changes: 41 additions & 0 deletions examples/jwt_iss_fingerprint.rb
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

0 comments on commit a02c35e

Please sign in to comment.