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 ¶
- func Migrate(ctx context.Context, db *stdsql.DB, driver string, opts ...Option) error
- type Option
- type Revoker
- func (r *Revoker) IsTokenRevoked(ctx context.Context, jti string) (bool, error)
- func (r *Revoker) Prune(ctx context.Context) (tokens, users int64, err error)
- func (r *Revoker) RevokeToken(ctx context.Context, jti string, expiresAt time.Time) error
- func (r *Revoker) RevokeUser(ctx context.Context, userID string, cutoff, retainUntil time.Time) error
- func (r *Revoker) UserCutoff(ctx context.Context, userID string) (time.Time, error)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Migrate ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.