Class: Doorkeeper::OAuth::ClientAuthentication::PrivateKeyJwt::ReplayGuard

Inherits:
Object
  • Object
show all
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

Constructor Details

#initializeReplayGuard

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

#clearObject



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).

Returns:

  • (Boolean)

    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