Skip to content

3.0.0: reuse_detection is silently inert when hash_tokens is enabled (spent registry keyed on the stored hash, lookup keyed on the raw token) #433

Description

@sanchobouillant

Version: gesdinet/jwt-refresh-token-bundle 3.0.0 · Symfony 8.1.5 · PHP 8.4/8.5 · Doctrine ORM 3.6 (Postgres) · lexik/jwt-authentication-bundle 3.2.0

Summary

With both hash_tokens.enabled: true and reuse_detection.enabled: true, replaying a spent single-use refresh token is rejected (401) but never revokes the family: the detection never fires, with no error and no log.

Cause (read in the source)

  • AttachRefreshTokenOnSuccessListener.php:258 calls remember($refreshToken) with the model.
  • CacheSpentRefreshTokenRegistry::remember() keys the entry on sha256($refreshToken->getRefreshToken()). When hash_tokens is on, getRefreshToken() returns the stored hash (as the listener itself notes around line 304: "a manager that hashes the token keeps the hash on the model"), so the key is sha256(hash).
  • On replay, RefreshTokenAuthenticator.php:102 calls unknownTokenPresented($token) with the raw token from the request, and recall() looks up sha256(raw).
  • sha256(hash) !== sha256(raw) → nothing is ever recalled.

Reproduction (deterministic — one key changed, everything else identical)

Config: single_use: true, single_use_ttl_update: true, reuse_detection: { enabled: true, cache: cache.app }, Doctrine storage.

  1. login → RT1
  2. refresh with RT1 → 200, RT2
  3. refresh with RT1 again (replay) → 401 (expected, single use)
  4. refresh with RT2 (valid, never used) →
    • hash_tokens.enabled: false → 401 — family revoked ✅
    • hash_tokens.enabled: true → 200 — family alive ❌ (detection inert)

Verified twice independently (author + reviewer) on the same code base.

Expected

Either the registry keys on the raw token on both sides (the raw value is available in the request when the token is spent), or the two options are declared mutually exclusive at configuration time (like cache_pool vs reuse_detection already is), so the failure is loud instead of silent.

Workaround we chose

Keep hash_tokens: true and set reuse_detection.enabled: false explicitly (with a comment) until fixed. Happy to test a patch.

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions