sql

package
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 3, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package sql provides a SQL-backed implementation of middleware/auth.Revoker, so that a logout performed on one replica is seen by every other replica — and survives a restart, which an in-process blocklist does not.

It is the option for a deployment that already has Postgres or SQLite and does not want a second piece of infrastructure for one small table. Where Redis is already in the stack, middleware/auth/redis does the same job with server-side key expiry and no sweeping; this package trades that for using the database the application already runs.

if err := authsql.Migrate(ctx, db, "postgres"); err != nil { ... }
rev := authsql.NewRevoker(db)
server.Pipeline.Auth.Register(auth.JWTAuth(secret, auth.JWTOptions{Revoker: rev}))
server.Action(auth.Logout(rev, ""))
server.Action(auth.LogoutAll(rev, "", 24*time.Hour))

It lives in the core module because it needs nothing but database/sql — there is no driver dependency here, and none is imported.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Migrate

func Migrate(ctx context.Context, db *stdsql.DB, driver string, opts ...Option) error

Migrate creates the two blocklist tables and their indexes if they do not exist. Call it once at startup, before NewRevoker. driver must be "postgres" or "sqlite". Pass the same options given to NewRevoker.

It is safe to run on every boot and from several replicas at once: every statement is IF NOT EXISTS, and none of them rewrites an existing column.

Types

type Option

type Option func(*config)

Option configures a Revoker (and, via the same options, Migrate).

func WithDriver

func WithDriver(name string) Option

WithDriver forces the SQL dialect instead of detecting it from the *sql.DB's registered driver. Accepts "postgres"/"postgresql"/"pgx" and "sqlite"/"sqlite3"; anything else falls through to detection rather than silently forcing one.

func WithPruneEvery

func WithPruneEvery(n int) Option

WithPruneEvery sets how many writes pass between opportunistic sweeps of expired rows. Zero disables them, leaving Prune the only way rows are removed — choose that when write latency must be uniform, and schedule Prune yourself. Default: 128.

func WithTablePrefix

func WithTablePrefix(prefix string) Option

WithTablePrefix namespaces the two tables, so an application whose schema already owns those names — or which runs two independent blocklists — can move them. WithTablePrefix("auth_") gives "auth_revoked_token" and "auth_revoked_user". Index names are derived from the table names so they do not collide either.

Pass the same option to both Migrate and NewRevoker. The resulting names must be plain SQL identifiers ([A-Za-z_][A-Za-z0-9_]*): they are interpolated directly into every statement, which cannot bind them as parameters. Migrate reports a bad one as an error and NewRevoker panics. Do not build this from user input.

type Revoker

type Revoker struct {
	// contains filtered or unexported fields
}

Revoker is a SQL-backed JWT blocklist for middleware/auth. It is safe for concurrent use.

Two tables are used:

revoked_token(jti PRIMARY KEY, expires_at)     — one token, until its own exp
revoked_user (user_id PRIMARY KEY, cutoff, retain_until) — every token issued
                                                           before cutoff

A NULL deadline in either table means "never drop", which is what a token carrying no exp claim produces. That is the safe direction: the alternative is dropping the entry while the token it blocks is still usable.

Errors are returned rather than swallowed, which is what lets the middleware fail closed: during a database outage requests are refused with 503 instead of every revoked token quietly becoming valid again.

func NewRevoker

func NewRevoker(db *stdsql.DB, opts ...Option) *Revoker

NewRevoker returns a Revoker over db. Call Migrate once at startup first.

It panics if WithTablePrefix produced a name that is not a plain SQL identifier: the name is interpolated into every statement this Revoker issues, so there is no safe way to continue — falling back to the default table would silently read and write the wrong blocklist. Migrate reports the same condition as an error, having a return value to report it with.

func (*Revoker) IsTokenRevoked

func (r *Revoker) IsTokenRevoked(ctx context.Context, jti string) (bool, error)

IsTokenRevoked implements auth.Revoker.

The deadline is applied in the WHERE clause rather than trusted to have been swept, so a row that Prune has not reached yet still stops being honoured at exactly the right moment. Correctness never depends on the sweep.

func (*Revoker) Prune

func (r *Revoker) Prune(ctx context.Context) (tokens, users int64, err error)

Prune deletes every row past its deadline and reports how many went from each table. It is safe to call concurrently with normal traffic — a row still being honoured is never in range — and safe to call from several replicas at once.

The Revoker also sweeps on its own every WithPruneEvery writes. Call this directly when you want the failure reported (the opportunistic sweep discards it, so housekeeping cannot fail a logout) or on a schedule of your own:

scheduled.Every(time.Hour, func(ctx context.Context) error {
    _, _, err := rev.Prune(ctx)
    return err
})

func (*Revoker) RevokeToken

func (r *Revoker) RevokeToken(ctx context.Context, jti string, expiresAt time.Time) error

RevokeToken implements auth.Revoker.

A repeat revocation of the same jti keeps the later deadline, and keeps NULL once either side is NULL, so re-revoking can only ever extend how long the entry is honoured.

func (*Revoker) RevokeUser

func (r *Revoker) RevokeUser(ctx context.Context, userID string, cutoff, retainUntil time.Time) error

RevokeUser implements auth.Revoker.

The cutoff is only ever moved forward, and unlike a read-then-write that rule is enforced by the statement itself: the upsert takes the greater of the stored and incoming values, so two concurrent revocations cannot resurrect tokens by landing out of order.

func (*Revoker) UserCutoff

func (r *Revoker) UserCutoff(ctx context.Context, userID string) (time.Time, error)

UserCutoff implements auth.Revoker.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL