Help us shape the future of Go! Take the 2026 Go Developer Survey

sessions

package
v0.40.22 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 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

View Source
const (
	KindWeb = "web"
	KindApp = "app"
)

Session kinds (sessions.kind).

View Source
const AppTTL = 90 * 24 * time.Hour

AppTTL is an iPhone app session's lifetime, sliding from its last use (iOS design §2, DA's condition 5).

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) LookupBearer added in v0.38.11

func (a *AuthAdapter) LookupBearer(ctx context.Context, rawToken string) (auth.SessionInfo, error)

LookupBearer implements auth.SessionStore for the iPhone app's token: app sessions only, never a browser cookie value.

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
	// Kind is "web" (the browser cookie, the default) or "app" (a bearer
	// token held by the iPhone app, migration 0076).
	Kind 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
	// Kind is KindWeb (a browser cookie) or KindApp (the iPhone app's token).
	Kind 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, most recently used first. Backs the device list.

func (*Store) Live added in v0.40.19

func (s *Store) Live(ctx context.Context, userID, id uuid.UUID) (created time.Time, ok bool, err error)

Live returns when the session id of userID was created, and whether it is still usable (not revoked, not expired).

func (*Store) LiveByIDHash added in v0.40.19

func (s *Store) LiveByIDHash(ctx context.Context, userID uuid.UUID, idHash []byte) (bool, error)

LiveByIDHash is Live for a session known only by sha256 of its id's text (what the Apple link request keeps instead of the id).

func (*Store) Lookup

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

Lookup resolves a token to a live Session of the given kind. Returns ErrInvalidToken for every failure mode — bad format, not found, revoked, expired, other kind — so the middleware can't accidentally tell an attacker which is which. The kind keeps the two credentials apart in both directions (DA on #769): the cookie path asks for KindWeb, so an iPhone app token is no cookie; the bearer path asks for KindApp, so a browser cookie value is no bearer token.

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) RevokeAll added in v0.39.3

func (s *Store) RevokeAll(ctx context.Context, userID uuid.UUID) (int64, error)

RevokeAll revokes every live session of userID, browsers and iPhone app alike (Apple told us the user withdrew consent or deleted their Apple account), and returns how many it revoked.

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).

func (*Store) RevokeOthers added in v0.38.12

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

RevokeOthers revokes every live session of userID except keepID and returns how many it revoked ("sign out everywhere else").

func (*Store) RevokeOwned added in v0.38.12

func (s *Store) RevokeOwned(ctx context.Context, userID, id uuid.UUID) (bool, error)

RevokeOwned revokes one live session if it belongs to userID and reports whether it did. Another user's id, an unknown id and an already revoked or expired session all read as false, so a caller cannot tell them apart.

Jump to

Keyboard shortcuts

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