sessions

package
v0.38.3 Latest Latest
Warning

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

Go to latest
Published: Oct 20, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package sessions is the source-of-truth store for authenticated user sessions. A row in `sessions` represents a cookie that has been issued to a browser; the cookie itself carries a random 32-byte opaque token, and the server keeps only an HMAC of that token so a DB dump doesn't grant session access without also holding the pepper.

This replaces the old HMAC-signed-cookie-carrying-{user_id, exp} design. See ARCHITECTURE.md decision 4 for the rationale.

Why HMAC(pepper, raw) and not bcrypt / argon2

Session tokens are 32 bytes of cryptographic randomness; they are not passwords. Offline attack resistance is dominated by entropy (256 bits) rather than work factor. HMAC-SHA256 with a host-local pepper is cheap (no login latency cost), constant time, and defeats the only realistic attack — a DB reader who doesn't also own the host env.

Why Postgres and not Redis

The lookup path is one indexed SELECT per authenticated request. At 1000 vehicles that's ~30 req/s peak, well within a small Postgres instance's budget. Redis would buy us one less RTT but cost a second data store to back-up, upgrade, and reason about. Not worth it until the p50 API latency tells us it is.

Index

Constants

This section is empty.

Variables

View Source
var ErrInvalidToken = errors.New("sessions: invalid token")

ErrInvalidToken is returned when a token doesn't parse or doesn't match any live session row. Intentionally opaque — the middleware translates it to a 401 without a reason, so an attacker can't distinguish "bad format" from "revoked" from "expired".

Functions

func SanitizeIP

func SanitizeIP(remoteAddr string) string

SanitizeIP trims a net.RemoteAddr form ("ip:port" or "[ip]:port") into a plain address suitable for storage. Exported so the auth package can feed raw r.RemoteAddr in without re-implementing the parse.

Types

type AuthAdapter

type AuthAdapter struct{ *Store }

AuthAdapter wraps *Store so it satisfies auth.SessionStore. The adapter lives here (not in the auth package) so the auth package stays free of a concrete sessions dependency; auth programs against its own SessionStore interface.

The two tiny struct translations (AuthCreateOpts → CreateOpts, Session → auth.SessionInfo) are the whole reason this type exists — if the shapes ever converge we can delete the adapter.

func NewAuthAdapter

func NewAuthAdapter(s *Store) *AuthAdapter

NewAuthAdapter wraps an existing Store for use with auth.Service.WithSessionStore.

func (*AuthAdapter) Create

Create implements auth.SessionStore.

func (*AuthAdapter) Lookup

func (a *AuthAdapter) Lookup(ctx context.Context, rawTokenCookie string) (auth.SessionInfo, error)

Lookup implements auth.SessionStore.

func (*AuthAdapter) RevokeByToken

func (a *AuthAdapter) RevokeByToken(ctx context.Context, rawTokenCookie string) error

RevokeByToken implements auth.SessionStore.

type CreateOpts

type CreateOpts struct {
	UserAgent   string
	IPAddress   string // textual form; Create parses it
	DeviceLabel string
}

CreateOpts captures the metadata to stamp onto a new session row. All fields are optional; zero values land in the DB as NULL / empty.

type Session

type Session struct {
	ID          uuid.UUID
	UserID      uuid.UUID
	CreatedAt   time.Time
	LastSeenAt  time.Time
	ExpiresAt   time.Time
	UserAgent   string
	IPAddress   string
	DeviceLabel string
}

Session is the in-memory projection of a row. Callers use it to render the device list and to populate request context.

type Store

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

Store is the sessions persistence layer. Pepper is an HMAC key used to hash tokens before storage; it must be stable across restarts or every session will be invalidated, and it must live outside the DB (env var / KMS) so a DB dump can't be used to forge sessions.

func New

func New(db *sql.DB, pepper []byte) (*Store, error)

New builds a Store. Pepper must be >= 32 bytes; shorter is refused at construction so operators don't ship with a trivially-brute-forceable setup.

func (*Store) Compare

func (s *Store) Compare(raw, hash []byte) bool

Compare is exported for any future call site that needs to check a raw token against an already-loaded hash in constant time (e.g. a secondary path that avoided the index).

func (*Store) Create

func (s *Store) Create(ctx context.Context, userID uuid.UUID, ttl time.Duration, opts CreateOpts) (Session, string, error)

Create mints a fresh session for userID and returns:

  • Session: the DB row projection.
  • rawToken: the value that goes into the Set-Cookie header. The raw token is only visible on Create — we never store it, only its HMAC. Losing track of it means the cookie can't be issued.

func (*Store) List

func (s *Store) List(ctx context.Context, userID uuid.UUID) ([]Session, error)

List returns every live session for a user, newest first. Backs the future device-list UI.

func (*Store) Lookup

func (s *Store) Lookup(ctx context.Context, rawTokenCookie string) (Session, error)

Lookup resolves a cookie-borne token to a live Session. Returns ErrInvalidToken for every failure mode — bad format, not found, revoked, expired — so the middleware can't accidentally tell an attacker which is which.

As a side effect, Lookup bumps last_seen_at if more than touchInterval has passed since the last bump. The update is best-effort: a failure is logged by the caller but the session still resolves, because refusing authentication due to a stat-tracking failure would be absurd.

func (*Store) PurgeExpired

func (s *Store) PurgeExpired(ctx context.Context, grace time.Duration) (int64, error)

PurgeExpired deletes revoked / expired rows older than a grace period. Runs from a background janitor; not in the hot path. Grace window exists so support can forensic-inspect recent revocations.

func (*Store) Revoke

func (s *Store) Revoke(ctx context.Context, id uuid.UUID) error

Revoke marks a specific session as revoked. Idempotent: a missing row is treated as success (matches "logout from a cookie that was already invalidated server-side").

func (*Store) RevokeAllExcept

func (s *Store) RevokeAllExcept(ctx context.Context, userID, keepID uuid.UUID) error

RevokeAllExcept revokes every session for userID except the caller's current one. Used by the future "sign out everywhere else" UI; also a useful panic button when a password changes.

func (*Store) RevokeByToken

func (s *Store) RevokeByToken(ctx context.Context, rawTokenCookie string) error

RevokeByToken is the convenience called by the /logout handler — translates the cookie value to a row id and revokes it in a single round-trip. Garbage tokens resolve to nothing and are silently ignored (the user's cookie was already useless).

Jump to

Keyboard shortcuts

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