Class: RackJwtVerifier::ReplayGuard

Inherits:
Object
  • Object
show all
Defined in:
lib/rack_jwt_verifier/replay_guard.rb

Overview

Records each verified token's jti in a cache store until the token expires, and rejects a token whose jti is already there.

The store is the same read/write abstraction used for key material (InProcessCache, Rails.cache, ...). An in-process store only detects replays within one worker; use a shared store for real protection.

Failure mode is closed: a store that raises turns into ReplayCacheError (a 503 in the middleware). The operator asked for replay protection, so a request whose jti cannot be checked is not waved through.

Constant Summary collapse

CACHE_KEY_PREFIX =
"rack_jwt_verifier:jti"
FALLBACK_TTL =

Used when a payload has no numeric exp to derive the TTL from (only possible when the operator removed exp from required_claims).

24 * 60 * 60

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(store, leeway: 0) ⇒ ReplayGuard

Returns a new instance of ReplayGuard.

Parameters:

  • the cache store holding seen jtis.

  • (defaults to: 0)

    clock-skew leeway; a jti is remembered for exp + leeway.



28
29
30
31
# File 'lib/rack_jwt_verifier/replay_guard.rb', line 28

def initialize(store, leeway: 0)
  @store = store
  @leeway = leeway
end

Instance Attribute Details

#leewayObject (readonly)

Returns the value of attribute leeway.



24
25
26
# File 'lib/rack_jwt_verifier/replay_guard.rb', line 24

def leeway
  @leeway
end

#storeObject (readonly)

Returns the value of attribute store.



24
25
26
# File 'lib/rack_jwt_verifier/replay_guard.rb', line 24

def store
  @store
end

Instance Method Details

#cache_key(jti) ⇒ String

Returns the cache key; jti is hashed so an attacker-chosen value cannot produce an unbounded or store-hostile key.

Parameters:

Returns:

  • the cache key; jti is hashed so an attacker-chosen value cannot produce an unbounded or store-hostile key.



56
57
58
# File 'lib/rack_jwt_verifier/replay_guard.rb', line 56

def cache_key(jti)
  "#{CACHE_KEY_PREFIX}:#{Digest::SHA256.hexdigest(jti)}"
end

#check!(payload) ⇒ Object

Parameters:

  • the verified claims.

Raises:

  • if this jti has been seen before.

  • if the payload carries no usable jti.

  • if the store cannot be read or written.



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/rack_jwt_verifier/replay_guard.rb', line 37

def check!(payload)
  jti = payload["jti"].to_s
  raise JWT::InvalidJtiError, "Missing jti" if jti.strip.empty?

  key = cache_key(jti)
  ttl = ttl_for(payload)

  # Read first so stores that ignore unless_exist still catch the common
  # case; the conditional write then closes the window for stores that
  # honour it (ActiveSupport stores return false when the key exists).
  seen, stored = store_access(key, ttl)
  raise ReplayedTokenError, "Token has already been used (jti #{jti})" if seen || stored == false

  nil
end