asobi_auth_cache (asobi v0.72.5)
View SourceIn-memory cache for access-token → player resolution.
The WS connect path and every authenticated HTTP request hit
nova_auth_refresh:get_user_by_access_token/2, which costs two
kura queries (one for the token row, one for the user). Under
mobile-reconnect storms this is the dominant per-connect cost. The
cache turns a hit into one ETS lookup.
Lifetime and freshness
Entries TTL after asobi.auth_cache_ttl_ms (default 60_000ms) so a
revoked token's fallout is bounded. Invalidation must be wired into
every code path that deletes or replaces a token; see invalidate/1.
Negative results (token not found, expired) are also cached but with
a much shorter TTL (asobi.auth_cache_negative_ttl_ms, default
5_000ms) so a fresh-token race doesn't keep returning errors after
the token actually exists.
Revocation SLA (asobi#215)
A revoked token's cache entry can outlive its DB row by up to
auth_cache_ttl_ms unless the revoking code path also touches this
cache. Audited as of asobi#215:
- Single-device logout (
asobi_auth_tokens:revoke_access/1) callsinvalidate/1for the one access token being logged out. Immediate. - Mass revoke keyed by player (any path that deletes more tokens
than the caller holds a single token for - e.g.
asobi_guest_controller's guest-to-real upgrade, which callsnova_auth_refresh:revoke_all/2for every token a player holds, or a future ban/admin-suspend path) MUST callrevoke_player/1right after the DB-level revoke, passing the player id. The stored value carriesid(seecacheable/1), so a match-spec scan can target every cached entry for one player without evicting every other player's session; there is no need to reach forclear/0here. "Targeted" is about blast radius, not cost: there is no secondary index onid, sorevoke_player/1is still an O(total cache size) scan, same shape asclear/0'sdelete_all_objects/1, not O(that player's token count). clear/0remains available for whole-table maintenance (test teardown, an operator-triggered flush) but is intentionally NOT the revocation primitive - a full clear scales with total cached sessions rather than the one player being revoked, and is an easy denial-of-service amplifier if wired to a client-reachable action.- Refresh-token rotation (
nova_auth_refresh:refresh/2) does NOT revoke the access token that was live before the rotation - bynova_auth's design, access tokens are short-lived bearer credentials (60 min default,asobi_auth:config/0) that aren't tied to their refresh token's rotation state, so there is nothing to invalidate here even in principle. This is anova_authdesign characteristic, not anasobi_auth_cachegap.
Revoke/read race
invalidate/1 and revoke_player/1 only delete what's in the table
right now. A miss/1 already in flight (DB read returned, put_positive/2
not yet called) can still land its write after the revoke, caching the
stale row for a fresh TTL. epoch/0 closes this: every revoke bumps a
counter first, and miss/1 only caches its read if the epoch it read
before querying the DB is still current when the DB answers - otherwise
a revoke happened mid-read and the result is served once, uncached.
Process model
A single named gen_server owns the ETS table. Lookups are direct ETS
reads from any process; writes go through the gen_server only for
expiry-sweep coordination — put/2,3, invalidate/1 and
revoke_player/1 write directly via public ETS for latency. The
gen_server runs a periodic sweep (every TTL/2) that removes expired
rows so the table doesn't grow unbounded under attack.
Summary
Functions
-spec clear() -> ok.
-spec handle_call(term(), gen_server:from(), map()) -> {reply, ok, map()}.
-spec info() -> #{size := non_neg_integer(), memory_words := non_neg_integer()}.
-spec init([]) -> {ok, map()}.
-spec invalidate(binary()) -> ok.
-spec put_negative(binary()) -> ok.
-spec revoke_player(binary()) -> ok.
-spec start_link() -> gen_server:start_ret().