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§
Required Methods§
Sourcefn store_token(
&self,
token: &RefreshToken,
) -> impl Future<Output = Result<(), Self::Error>> + Send
fn store_token( &self, token: &RefreshToken, ) -> impl Future<Output = Result<(), Self::Error>> + Send
Persist a new refresh token record.
Sourcefn find_token(
&self,
token_hash: &str,
) -> impl Future<Output = Result<Option<RefreshToken>, Self::Error>> + Send
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.
Sourcefn revoke_token(
&self,
token_id: &RefreshTokenId,
) -> impl Future<Output = Result<(), Self::Error>> + Send
fn revoke_token( &self, token_id: &RefreshTokenId, ) -> impl Future<Output = Result<(), Self::Error>> + Send
Mark a token as revoked by its ID.
Sourcefn revoke_user_tokens(
&self,
user_id: &UserId,
) -> impl Future<Output = Result<(), Self::Error>> + Send
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).
Sourcefn active_tokens(
&self,
user_id: &UserId,
) -> impl Future<Output = Result<Vec<RefreshToken>, Self::Error>> + Send
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.
Sourcefn revoke_family(
&self,
user_id: &UserId,
family_id: &TokenFamilyId,
) -> impl Future<Output = Result<(), Self::Error>> + Send
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 NULLTwo failure modes the contract is meant to prevent:
- Non-atomic load+update: a backend that lists active tokens
and then revokes them one-by-one races with a parallel
refresh_sessioncall from the attacker; the attacker may succeed in rotating an unrevoked sibling between the list and the per-row revoke, escaping the family invalidation. - Family scope ignored: delegating to
revoke_user_tokenswould 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.
Sourcefn issue_with_eviction(
&self,
evict_ids: &[RefreshTokenId],
new_token: &RefreshToken,
) -> 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
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.
Sourcefn rotate_token(
&self,
parent_id: &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
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.:
- A single SQL transaction wrapping
UPDATE … SET revoked_at+INSERT INTO refresh_tokens. - A Lua/Redis MULTI/EXEC block on Valkey-class stores.
- 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§
Sourcefn on_token_compromise(
&self,
user_id: &UserId,
family_id: &TokenFamilyId,
compromised_devices: &[(TenantId, DeviceId)],
) -> impl Future<Output = ()> + Send
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.
impl RefreshTokenStore for MemoryRefreshTokenStore
testing only.