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 ¶
- Variables
- func SanitizeIP(remoteAddr string) string
- type AuthAdapter
- func (a *AuthAdapter) Create(ctx context.Context, userID uuid.UUID, ttl time.Duration, ...) (auth.SessionInfo, string, error)
- func (a *AuthAdapter) Lookup(ctx context.Context, rawTokenCookie string) (auth.SessionInfo, error)
- func (a *AuthAdapter) RevokeByToken(ctx context.Context, rawTokenCookie string) error
- type CreateOpts
- type Session
- type Store
- func (s *Store) Compare(raw, hash []byte) bool
- func (s *Store) Create(ctx context.Context, userID uuid.UUID, ttl time.Duration, opts CreateOpts) (Session, string, error)
- func (s *Store) List(ctx context.Context, userID uuid.UUID) ([]Session, error)
- func (s *Store) Lookup(ctx context.Context, rawTokenCookie string) (Session, error)
- func (s *Store) PurgeExpired(ctx context.Context, grace time.Duration) (int64, error)
- func (s *Store) Revoke(ctx context.Context, id uuid.UUID) error
- func (s *Store) RevokeAllExcept(ctx context.Context, userID, keepID uuid.UUID) error
- func (s *Store) RevokeByToken(ctx context.Context, rawTokenCookie string) error
Constants ¶
This section is empty.
Variables ¶
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 ¶
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 ¶
func (a *AuthAdapter) Create(ctx context.Context, userID uuid.UUID, ttl time.Duration, opts auth.SessionCreateOpts) (auth.SessionInfo, string, error)
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 ¶
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 ¶
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 ¶
List returns every live session for a user, newest first. Backs the future device-list UI.
func (*Store) Lookup ¶
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 ¶
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 ¶
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 ¶
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 ¶
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).