Skip to main content

RefreshTokenStore

Trait RefreshTokenStore 

Source
pub trait RefreshTokenStore: Send + Sync {
    type Error: Error + Send + Sync + 'static;

    // Required methods
    fn store_token(
        &self,
        token: &RefreshToken,
    ) -> impl Future<Output = Result<(), Self::Error>> + Send;
    fn find_token(
        &self,
        token_hash: &str,
    ) -> impl Future<Output = Result<Option<RefreshToken>, Self::Error>> + Send;
    fn revoke_token(
        &self,
        token_id: &RefreshTokenId,
    ) -> impl Future<Output = Result<(), Self::Error>> + Send;
    fn revoke_user_tokens(
        &self,
        user_id: &UserId,
    ) -> impl Future<Output = Result<(), Self::Error>> + Send;
    fn active_tokens(
        &self,
        user_id: &UserId,
    ) -> impl Future<Output = Result<Vec<RefreshToken>, Self::Error>> + Send;
    fn revoke_family(
        &self,
        user_id: &UserId,
        family_id: &TokenFamilyId,
    ) -> impl Future<Output = Result<(), Self::Error>> + Send;
    fn issue_with_eviction(
        &self,
        evict_ids: &[RefreshTokenId],
        new_token: &RefreshToken,
    ) -> impl Future<Output = Result<(), Self::Error>> + Send;
    fn rotate_token(
        &self,
        parent_id: &RefreshTokenId,
        new_token: &RefreshToken,
    ) -> impl Future<Output = Result<(), Self::Error>> + Send;

    // Provided method
    fn on_token_compromise(
        &self,
        user_id: &UserId,
        family_id: &TokenFamilyId,
        compromised_devices: &[(TenantId, DeviceId)],
    ) -> impl Future<Output = ()> + Send { ... }
}
Expand description

Async storage backend for refresh tokens.

All methods accept &self; implementations use interior mutability.

Required Associated Types§

Source

type Error: Error + Send + Sync + 'static

The error type returned by storage operations.

Required Methods§

Source

fn store_token( &self, token: &RefreshToken, ) -> impl Future<Output = Result<(), Self::Error>> + Send

Persist a new refresh token record.

Source

fn find_token( &self, token_hash: &str, ) -> impl Future<Output = Result<Option<RefreshToken>, Self::Error>> + Send

Look up a token by its hash. Returns None if not found.

Source

fn revoke_token( &self, token_id: &RefreshTokenId, ) -> impl Future<Output = Result<(), Self::Error>> + Send

Mark a token as revoked by its ID.

Source

fn revoke_user_tokens( &self, user_id: &UserId, ) -> impl Future<Output = Result<(), Self::Error>> + Send

Revoke all tokens for a user (e.g. global logout, credential rotation).

Source

fn active_tokens( &self, user_id: &UserId, ) -> impl Future<Output = Result<Vec<RefreshToken>, Self::Error>> + Send

Return all non-revoked tokens for a user, ordered by issued_at ascending.

Source

fn revoke_family( &self, user_id: &UserId, family_id: &TokenFamilyId, ) -> impl Future<Output = Result<(), Self::Error>> + Send

Revoke all tokens in a family (same family_id).

Called when a rotated-out (already revoked) token is reused. This is a compromise signal indicating the original token was stolen. All tokens descending from the original issuance must be revoked immediately so that neither the attacker nor the legitimate user can continue refreshing.

§Contract

Implementations MUST perform the revocation as a single atomic statement scoped to family_id, e.g.:

UPDATE refresh_tokens
   SET revoked_at = NOW()
 WHERE user_id = $1
   AND family_id = $2
   AND revoked_at IS NULL

Two failure modes the contract is meant to prevent:

  1. Non-atomic load+update: a backend that lists active tokens and then revokes them one-by-one races with a parallel refresh_session call from the attacker; the attacker may succeed in rotating an unrevoked sibling between the list and the per-row revoke, escaping the family invalidation.
  2. Family scope ignored: delegating to revoke_user_tokens would over-revoke (safe, but boots the legitimate user out of every other concurrent device). With per-family scoping, only the compromised device chain is killed.
Source

fn issue_with_eviction( &self, evict_ids: &[RefreshTokenId], new_token: &RefreshToken, ) -> impl Future<Output = Result<(), Self::Error>> + Send

Atomically issue a new refresh token while evicting the per-user cap-overflow set in a single transaction.

The naive sequence (revoke_token(evict[i]) … then store_token(new)) is not atomic. If a store_token failure arrives after the evictions, the user has just lost N legitimate active tokens for nothing; if it arrives before, an in-flight concurrent issue could push the active set above the cap.

§Requirement

This method is required: implementations MUST wrap the per-id revocations and the new-token insert in a single transaction (or equivalent atomic primitive) so partial-failure semantics match; either all evictions land and the new token is stored, or nothing changes.

Source

fn rotate_token( &self, parent_id: &RefreshTokenId, new_token: &RefreshToken, ) -> impl Future<Output = Result<(), Self::Error>> + Send

Atomically rotate a refresh token: revoke parent_id and insert new_token in a single transaction.

§Why this exists

The naive sequence (revoke_token(parent) followed by store_token(new)) is not atomic. If the second call fails (network blip, DB write error, process crash between the two), the parent is already revoked and no replacement was issued. The user is silently logged out across every device using that token family, with no recovery path short of re-authentication.

§Contract

Implementations MUST wrap the parent-revoke and the new-token insert in a single transaction (or equivalent atomic primitive), e.g.:

  1. A single SQL transaction wrapping UPDATE … SET revoked_at + INSERT INTO refresh_tokens.
  2. A Lua/Redis MULTI/EXEC block on Valkey-class stores.
  3. Any equivalent atomic primitive provided by the backend.

A naive serial revoke + store can leave a user silently logged out across every device in the family if the second call fails mid-flight.

Provided Methods§

Source

fn on_token_compromise( &self, user_id: &UserId, family_id: &TokenFamilyId, compromised_devices: &[(TenantId, DeviceId)], ) -> impl Future<Output = ()> + Send

Called when a rotated-out token is reused (a token compromise signal).

Override this to alert operators (e.g. send to a SIEM, page on-call, or log at a higher severity). The default implementation logs a warning.

This is called after revoke_family has already revoked the compromised token family.

§Device cascade

compromised_devices carries every (TenantId, DeviceId) pair linked to refresh tokens in the compromised family: the already-revoked token under reuse plus any sibling members that were still active when reuse was detected. Production overrides SHOULD pass these to cascade_revoke_devices so every device that participated in the family transitions to DeviceTrustLevel::Revoked. The default implementation logs the device list alongside the family id; it does NOT perform the cascade itself because the trait has no access to a DeviceStore. The cascade primitive lives outside the refresh path so a single application can wire its own DeviceStore once and reuse it from any number of compromise hooks.

Dyn Compatibility§

This trait is not dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§

Source§

impl RefreshTokenStore for MemoryRefreshTokenStore

Available on crate features testing only.