Class: Doorkeeper::OAuth::ClientAuthentication::PrivateKeyJwt::ReplayGuard
- Inherits:
-
Object
- Object
- Doorkeeper::OAuth::ClientAuthentication::PrivateKeyJwt::ReplayGuard
- Includes:
- Singleton
- Defined in:
- lib/doorkeeper/oauth/client_authentication/private_key_jwt/replay_guard.rb
Overview
In-memory, process-local single-use guard for assertion jti values (OIDC Core §9: an assertion may only be used once). Entries live until the assertion's own exp, which PrivateKeyJwt caps at MAX_LIFETIME, so the memory held here is bounded: at most MAX_ENTRIES entries, each for at most MAX_LIFETIME seconds.
Being process-local this cannot catch a replay delivered to a different server process (separate Puma workers, separate hosts). Whether that matters depends on the deployment: the replay window is at most MAX_LIFETIME anyway, and an attacker who can capture an assertion in transit usually defeats TLS first. A deployment that wants cross-process replay protection supplies a shared store (backed by Redis or the like) through the private_key_jwt_replay_guard config option.
Constant Summary collapse
- MAX_ENTRIES =
Upper bound on remembered jti values. When the guard is full even after expired entries are pruned, the oldest entries are evicted rather than new assertions rejected — see first_use?.
10_000- SWEEP_INTERVAL =
Expired entries are swept periodically rather than on every authentication: the sweep is O(entries) and would otherwise run on each request, walking up to MAX_ENTRIES every time. An entry that outlives its exp by up to this long only makes the guard stricter, never more permissive.
10
Instance Method Summary collapse
- #clear ⇒ Object
-
#first_use?(key, expires_at:) ⇒ Boolean
True when the key was not seen before; the key is then remembered until
expires_at(unix time). -
#initialize ⇒ ReplayGuard
constructor
A new instance of ReplayGuard.
Constructor Details
#initialize ⇒ ReplayGuard
Returns a new instance of ReplayGuard.
38 39 40 41 42 |
# File 'lib/doorkeeper/oauth/client_authentication/private_key_jwt/replay_guard.rb', line 38 def initialize @mutex = Mutex.new @seen = {} @sweep_after = 0 end |
Instance Method Details
#clear ⇒ Object
68 69 70 71 72 73 |
# File 'lib/doorkeeper/oauth/client_authentication/private_key_jwt/replay_guard.rb', line 68 def clear @mutex.synchronize do @seen.clear @sweep_after = 0 end end |
#first_use?(key, expires_at:) ⇒ Boolean
Returns true when the key was not seen before; the key
is then remembered until expires_at (unix time).
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/doorkeeper/oauth/client_authentication/private_key_jwt/replay_guard.rb', line 46 def first_use?(key, expires_at:) now = Time.now.to_i @mutex.synchronize do if now >= @sweep_after || @seen.size >= MAX_ENTRIES @seen.delete_if { |_, expiry| expiry <= now } @sweep_after = now + SWEEP_INTERVAL end return false if @seen.key?(key) # When full even after expiry pruning, evict the oldest entries # rather than rejecting new assertions: rejecting would let a # flood of assertions lock legitimate clients out entirely, # while evicting only shortens the replay window under attack. @seen.shift while @seen.size >= MAX_ENTRIES @seen[key] = expires_at true end end |