Periodic housekeeping GenServer that deletes expired rows from the
Ecto-backed authorization-code, refresh-token, device-code, CIBA-request,
logout-session, DPoP-nonce, DPoP-replay, pushed-authorization-request,
client-id-metadata-cache, and consent-grant tables.
Each of these tables carries an expires_at column whose semantics are fixed
by the relevant RFC:
- authorization codes - RFC 6749 §4.1.2 ("The authorization code MUST expire shortly after it is issued") and §10.5 (codes are short-lived, single-use).
- refresh tokens - RFC 6749 §1.5 / §6 (refresh tokens MAY expire); the stored expiry bounds the credential's lifetime.
- server-issued DPoP nonces - RFC 9449 §8 / §9 (the
noncethe resource or authorization server requires the client to echo is time-bounded). - DPoP proof
jtireplay records - RFC 9449 §11.1 (ajtineed only be remembered for the proofiatacceptance window; past that window the record is dead weight). - pushed authorization requests - RFC 9126 §2.2 (a
request_urireference is short-lived; past its expiry it can resolve nothing). - cached Client ID Metadata Documents -
draft-ietf-oauth-client-id-metadata-document-01§6 / RFC 9111 (a cached document is fresh only until itsexpires_at; past that it is re-fetched). - consent grants - RFC 6749 §4.1.1 / §4.1.2 (consent precedes a short-lived
authorization code; a grant past its
expires_atcan authorize nothing, andconsume/2already rejects it on read). - back-channel-logout sessions - OpenID Connect Back-Channel Logout 1.0 (a
recorded
(session, RP)delivery row past itsexpires_atbelongs to an abandoned session and is no longer a logout target). - CIBA authentication requests - OpenID Connect CIBA Core 1.0 §7.3 (an
auth_req_idpast itsexpires_atyieldsexpired_tokenand can mint nothing;redeem/4already re-checks expiry on read).
Correctness vs. housekeeping
Expiry-row deletion is not required for authorization correctness. Every store re-validates
expires_at against the current time on read, so an expired row that has not
yet been swept is never honored: an expired authorization code is rejected, an
expired nonce is rejected, and an expired replay record no longer blocks a
fresh jti. Those deletes only bound table growth by reclaiming rows that can
no longer affect any decision. A consumed code's expired row can still carry
the replay-revocation link for a live access token, so that row is retained
until the linked token expires.
The process also irreversibly redacts refresh-successor ciphertext whose
short retry deadline has passed, on the next scheduled sweep. When the Ecto
refresh store uses a positive retry grace, this bounded credential cleanup
requires the sweeper. The
installer adds it to the host supervision tree automatically; manually wired
applications MUST supervise it with a positive :sweep_interval_ms.
The remaining work is TTL housekeeping: it issues one delete per swept table
using expires_at < $now. Authorization-code cleanup additionally keeps a
row while its non-empty access-token link has a future
access_token_expires_at, preserving replay revocation until that token dies.
Comparison boundary (fail-closed)
Row expiry uses a strict < comparison against a single DateTime captured
once per sweep (DateTime.utc_now/0) and reused across every table, so a
sweep applies one consistent boundary. A row whose expires_at equals "now"
is retained. For an already-expired authorization-code row, however, a
linked access token whose own expiry equals "now" is no longer live and does
not delay cleanup. The sweeper widens no acceptance window.
Configuration
All policy is read from AttestoPhoenix.Config; nothing is hardcoded here.
:repo- theEcto.Repothe deletes run against (required byAttestoPhoenix.Config).:sweep_interval_ms- how often a sweep runs, in milliseconds. Manual supervision fails fast when this key is unset. The installer uses the explicit:if_configuredmode so a rerun against an older or custom-store host leaves the child ignored when no interval was configured.:schema_prefix- optional PostgreSQL schema applied to every delete so a host that installed the generated tables under a non-default schema sweeps the same tables it created.
The set of swept tables is fixed by the generated schema and is not
host-configurable: every Ecto-backed store the library generates carries an
expires_at column and is swept.
Summary
Functions
Starts the sweeper.
Runs a single sweep synchronously and returns the number of rows deleted per table. Test- and diagnostic-facing; the supervised process drives sweeps via the configured interval, not this call.
Functions
@spec start_link(keyword()) :: GenServer.on_start()
Starts the sweeper.
Requires a %AttestoPhoenix.Config{} under the :config key. The config's
:sweep_interval_ms MUST be a positive integer; a missing or non-positive
interval raises ArgumentError so a misconfigured host fails at boot instead
of starting a process that never sweeps.
The installer passes :if_configured as true for upgrade compatibility.
In that mode an absent interval returns :ignore, allowing an existing host
that does not use the bundled Ecto stores to keep the sweeper disabled. An
invalid non-nil interval still raises, and direct/manual supervision retains
the fail-fast default.
@spec sweep_now() :: %{optional(String.t()) => non_neg_integer()}
Runs a single sweep synchronously and returns the number of rows deleted per table. Test- and diagnostic-facing; the supervised process drives sweeps via the configured interval, not this call.
@spec sweep_now(GenServer.server()) :: %{optional(String.t()) => non_neg_integer()}