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
- 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) LookupBearer(ctx context.Context, rawToken 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) Live(ctx context.Context, userID, id uuid.UUID) (created time.Time, ok bool, err error)
- func (s *Store) LiveByIDHash(ctx context.Context, userID uuid.UUID, idHash []byte) (bool, error)
- func (s *Store) Lookup(ctx context.Context, rawToken, kind 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) RevokeAll(ctx context.Context, userID uuid.UUID) (int64, error)
- func (s *Store) RevokeAllExcept(ctx context.Context, userID, keepID uuid.UUID) error
- func (s *Store) RevokeByToken(ctx context.Context, rawTokenCookie string) error
- func (s *Store) RevokeOthers(ctx context.Context, userID, keepID uuid.UUID) (int64, error)
- func (s *Store) RevokeOwned(ctx context.Context, userID, id uuid.UUID) (bool, error)
Constants ¶
const ( KindWeb = "web" KindApp = "app" )
Session kinds (sessions.kind).
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 ¶
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) 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 ¶
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, 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
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 ¶
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 ¶
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) RevokeAll ¶ added in v0.39.3
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 ¶
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).
func (*Store) RevokeOthers ¶ added in v0.38.12
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
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.