// Package auth is the session layer: ONE server-side session (D39.84), presented either as a // __Host cookie by the browser or as a Bearer token by the desktop and CLI clients. No handler may // look at either — the principal is created in middleware and read from the request context. package auth import ( "context" "crypto/rand" "crypto/sha256" "encoding/base64" "errors" "time" ) // CookieName is the browser presentation. The __Host prefix is not decoration: it forbids a Domain // attribute and requires Secure + Path=/, which is what stops a sibling subdomain from writing a // session cookie for the app. const CookieName = "__Host-tm_session" // tokenBytes is 256 bits of entropy (STACK_DECISIONS §5). Opaque: it encodes nothing, so a stolen // token cannot be read, and a JWT's "verify without the database" property is exactly what we do // not want — revocation must be immediate. const tokenBytes = 32 // ErrNoSession is returned when nothing live matches the presented token. Callers must not // distinguish "unknown token" from "expired token" on the wire: the difference is an oracle. var ErrNoSession = errors.New("auth: no live session") // Session is what the store knows about a live session. type Session struct { UserID string IdleExpiresAt time.Time AbsoluteExpiresAt time.Time } // SessionStore is the persistence the middleware needs. Lookup MUST apply expiry and revocation // itself, so a store that forgets a clause cannot be papered over here. type SessionStore interface { Lookup(ctx context.Context, digest []byte, now time.Time) (Session, error) Touch(ctx context.Context, digest []byte, now time.Time, idleTTL time.Duration) error } // NewToken mints a credential. The plaintext exists only in this return value and in the client: // what reaches the database is Digest(token). No error return — crypto/rand.Read never fails, it // crashes the program instead. func NewToken() string { b := make([]byte, tokenBytes) rand.Read(b) return base64.RawURLEncoding.EncodeToString(b) } // Digest is the stored form. Plain SHA-256 with no salt or stretching is correct HERE and only // here: the input is 256 uniform random bits, so there is no guessable space to slow an attacker // down in — unlike a password. func Digest(token string) []byte { sum := sha256.Sum256([]byte(token)) return sum[:] }