Documentation
¶
Overview ¶
Package refreshpg implements auth.RefreshStore on top of Postgres via the kit's db.Querier interface. The DDL lives in schema.sql next to this file; downstream services run it through their own migration tool.
Index ¶
- type ConsumeReusedHook
- type FamilyRevokeHook
- type IPRevokeHook
- type Option
- type SessionInfo
- type Store
- func (s *Store) Consume(ctx context.Context, tokenHash [32]byte, now time.Time) (auth.Record, error)
- func (s *Store) GarbageCollect(ctx context.Context, now time.Time) (int64, error)
- func (s *Store) GarbageCollectBatch(ctx context.Context, now time.Time, limit, maxIterations int) (int64, error)
- func (s *Store) Issue(ctx context.Context, r auth.Record) error
- func (s *Store) ListBySubject(ctx context.Context, subject string) ([]SessionInfo, error)
- func (s *Store) RevokeByIP(ctx context.Context, ip string) (int64, error)
- func (s *Store) RevokeFamily(ctx context.Context, familyID string) error
- func (s *Store) RevokeSubject(ctx context.Context, subject string) error
- func (s *Store) RevokeToken(ctx context.Context, tokenHash [32]byte, now time.Time) (auth.Record, bool, error)
- func (s *Store) Stats(ctx context.Context) (StoreStats, error)
- type StoreStats
- type SubjectRevokeHook
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ConsumeReusedHook ¶
ConsumeReusedHook fires INSIDE Consume when the kit detects a refresh token that has already been consumed or revoked — the OAuth 2.1 reuse-detection signal. RevokeFamily runs FIRST (the security-critical side effect happens regardless of hook behaviour); the hook only sees the event after the family has already been wiped.
Wire it to your SIEM / Sentry / pager — a non-zero rate is the canonical "stolen refresh token" alert. Panic-safe.
type FamilyRevokeHook ¶
FamilyRevokeHook fires after a successful RevokeFamily. `count` is the number of rows revoked (0 = idempotent no-op). Panic-safe.
type IPRevokeHook ¶
IPRevokeHook fires after a successful RevokeByIP. Panic-safe. Use for incident-response audit trails.
type Option ¶
type Option func(*storeOpts)
Option tunes the constructed *Store. The variadic shape on New keeps the zero-option call site (`refreshpg.New(db)`) compatible.
func WithLogger ¶
WithLogger wires a slog logger. Used for hook panic recovery and (sparingly) for diagnostic warnings — store ops are otherwise silent. nil = silent.
func WithMetrics ¶
func WithMetrics(reg prometheus.Registerer) Option
WithMetrics enables Prometheus instrumentation. The store registers:
- refreshpg_ops_total{op,outcome} — Issue / Consume / RevokeFamily / RevokeSubject / RevokeByIP / GarbageCollect / Stats / ListBySubject; outcome=ok|error (consume also: missing| expired|reused)
- refreshpg_op_duration_seconds{op} — wall-clock latency
Pass the same Registerer you give to other kit subsystems for a single /metrics scrape.
func WithOnConsumeReused ¶
func WithOnConsumeReused(fn ConsumeReusedHook) Option
WithOnConsumeReused registers a callback fired on every refresh reuse-detection event. See ConsumeReusedHook. Multiple calls — last wins.
func WithOnFamilyRevoke ¶
func WithOnFamilyRevoke(fn FamilyRevokeHook) Option
WithOnFamilyRevoke registers a post-RevokeFamily callback. See FamilyRevokeHook. Multiple calls — last wins.
func WithOnIPRevoke ¶
func WithOnIPRevoke(fn IPRevokeHook) Option
WithOnIPRevoke registers a post-RevokeByIP callback. See IPRevokeHook. Multiple calls — last wins.
func WithOnSubjectRevoke ¶
func WithOnSubjectRevoke(fn SubjectRevokeHook) Option
WithOnSubjectRevoke registers a post-RevokeSubject callback. See SubjectRevokeHook. Multiple calls — last wins.
type SessionInfo ¶
type SessionInfo struct {
FamilyID string
Subject string
IssuedAt time.Time
ExpiresAt time.Time
ConsumedAt time.Time // zero = still live
RevokedAt time.Time // zero = active
UserAgent string
IP string
State string // "active" | "consumed" | "revoked" | "expired"
}
SessionInfo is the admin-side projection of an issued refresh-token row. Returned by Store.ListBySubject. NEVER includes `token_hash` — the hash is secret material and stays in the store.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the Postgres-backed RefreshStore. Pool ownership stays with the caller. Observability + lifecycle hooks are opt-in via Option.
func New ¶
New takes any db.Querier (so callers can pass a *db.DB or a *db.Tx). The most common case — long-lived service — passes *db.DB. Trailing options enable metrics / logging / hooks; the zero-option form is unchanged from earlier versions.
func (*Store) Consume ¶
func (s *Store) Consume(ctx context.Context, tokenHash [32]byte, now time.Time) (auth.Record, error)
Consume atomically marks a refresh token consumed if it is live, valid, and not expired; otherwise it diagnoses the failure mode and (for reuse) revokes the whole family before returning the error.
The implementation is two queries: a single UPDATE ... RETURNING that succeeds for the live-and-valid case, and a follow-up SELECT only in the failure case to disambiguate not_found / expired / reused.
func (*Store) GarbageCollect ¶
GarbageCollect deletes records with expires_at <= now. Returns the number of rows removed.
func (*Store) GarbageCollectBatch ¶
func (s *Store) GarbageCollectBatch(ctx context.Context, now time.Time, limit, maxIterations int) (int64, error)
GarbageCollectBatch is the chunked variant of Store.GarbageCollect for very large tables: it loops `DELETE … WHERE expires_at <= now LIMIT N` until the affected count is zero, returning the total number of rows removed. Use when Store.GarbageCollect would lock the table for too long under nightly cron pressure.
`limit ≤ 0` is treated as 1000. `maxIterations ≤ 0` defaults to 1024 (a defence against pathological GC loops). On ctx cancel the method returns whatever it has already deleted plus ctx.Err() — partial progress is the safe behaviour for a sweeper.
func (*Store) Issue ¶
Issue inserts a row. pgx errors are funneled through db/'s mapPgxErr (via the Querier), so a unique-key collision returns *errs.Error{KindAlreadyExists}.
func (*Store) ListBySubject ¶
ListBySubject returns every refresh-token row for the subject ordered by `issued_at DESC`. Surface this to admin UIs that render an "active sessions" list — UI filters by `State` field to hide history.
Empty subject returns an empty slice without touching the DB.
func (*Store) RevokeByIP ¶
RevokeByIP revokes every currently-active refresh token issued from the supplied IP address. Use for incident response — a leaked session cookie or a compromised egress IP — to evict every active family bound to that address in one round trip.
Returns the number of rows revoked. Zero is NOT an error (idempotent against repeat calls and against IPs that never issued). Empty `ip` returns 0 without touching the DB.
The supplied string is parsed as an `inet` literal by Postgres — callers can pass either a v4 address ("203.0.113.7") or a v6 ("2001:db8::1"). Mask/CIDR forms are not supported (single-host semantics are what incident response actually wants).
func (*Store) RevokeFamily ¶
RevokeFamily marks every live token in the family as revoked. Idempotent via `COALESCE(revoked_at, now())` + `WHERE revoked_at IS NULL`.
func (*Store) RevokeSubject ¶
RevokeSubject revokes every live token belonging to the subject. Idempotent.
func (*Store) RevokeToken ¶ added in v1.2.0
func (s *Store) RevokeToken(ctx context.Context, tokenHash [32]byte, now time.Time) (auth.Record, bool, error)
RevokeToken marks the single matching record revoked — the store half of auth.TokenRevoker. Idempotent: an already-revoked record keeps its original revoked_at (COALESCE) and is still returned; a missing record is (Record{}, false, nil). Single UPDATE … RETURNING round trip.
type StoreStats ¶
StoreStats is the rollup returned by Store.Stats. Buckets are disjoint:
Active = consumed_at IS NULL AND revoked_at IS NULL AND expires_at > NOW() Consumed = consumed_at IS NOT NULL AND revoked_at IS NULL Revoked = revoked_at IS NOT NULL (revoke wins over consume/expiry) Expired = consumed_at IS NULL AND revoked_at IS NULL AND expires_at <= NOW()
Total = Active + Consumed + Revoked + Expired.