Revoke access tokens too on refresh-token reuse detection

revoke_family! revoked only the refresh tokens in a rotation family. When reuse
of a revoked refresh token was detected (a token-theft signal), the access
tokens issued across that chain stayed valid at /userinfo until expiry — up to
the access-token TTL — so an attacker holding a stolen access token kept access.

revoke_family! now also revokes every access token referenced by the family's
refresh tokens. Adds a regression test: rotate once, reuse the revoked token,
and assert both the original and rotated-in access tokens are revoked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dan Milne
2026-06-11 20:23:17 +10:00
parent cd862c7cd7
commit 24266872f9
2 changed files with 56 additions and 2 deletions

View File

@@ -49,11 +49,21 @@ class OidcRefreshToken < ApplicationRecord
update!(revoked_at: Time.current)
end
# Revoke all refresh tokens in the same family (token rotation security)
# Revoke all refresh tokens in the same family (token rotation security).
# Also revoke every access token issued within the family: on a detected reuse
# attack the stolen chain's access tokens must not remain usable at /userinfo
# until they expire.
def revoke_family!
return unless token_family_id.present?
OidcRefreshToken.in_family(token_family_id).update_all(revoked_at: Time.current)
now = Time.current
family = OidcRefreshToken.in_family(token_family_id)
access_token_ids = family.pluck(:oidc_access_token_id).compact.uniq
family.update_all(revoked_at: now)
if access_token_ids.any?
OidcAccessToken.where(id: access_token_ids, revoked_at: nil).update_all(revoked_at: now)
end
end
private