Hashing, the session store, the Manager
First the new dependency (bcrypt lives in the Go team's extended crypto):
go get golang.org/x/crypto/bcrypt
Five files, as tabs, in dependency order: 00002_sessions.sql → sessions.sql → users.sql → password.go → session.go (the table, its queries, the user queries register needs, the hasher, then the Manager that ties sessions together). Then run sqlc generate so the new queries become Go.
1. db/migrations/00002_sessions.sql — the session store. Note what's here and what isn't:
token_hash TEXT NOT NULL UNIQUE, -- we store the HASH, not the token
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at TIMESTAMPTZ NOT NULL
ON DELETE CASCADE means deleting a user removes their sessions automatically. token_hash is UNIQUE (one token → one session) and, crucially, it's the hash — the raw token never touches the database.
2. internal/db/queries/sessions.sql — CreateSession, GetUserBySessionToken (a join that resolves a token straight to a user, and filters expires_at > now()), and DeleteSession (logout, next lesson). GetUserBySessionToken uses sqlc.embed(users) so it returns a whole typed User.
3. internal/db/queries/users.sql (modify) — register needs two more queries: CreateUser (insert, RETURNING *) and GetUserByEmail (the duplicate check + login, next lesson). CountUsers from lesson 3 is untouched — the diff is pure additions.
4. internal/auth/password.go — HashPassword / CheckPassword. The length cap is the interesting line:
// MaxPasswordLen guards bcrypt's 72-byte input limit so passwords are never
// silently truncated. (A byte, not a rune, but 64 chars is plenty.)
MaxPasswordLen = 64
CheckPassword uses bcrypt.CompareHashAndPassword, which is constant-time — it can't be used as a timing oracle to guess the password.
5. internal/session/session.go — the Manager: Create (mint a random token, store its hash, set the cookie), User (cookie → hash → user, or ErrNoSession), Destroy (delete the row + expire the cookie). The cookie helper is where the flags live:
HttpOnly: true, // JS can't read it — mitigates XSS token theft
Secure: m.secure, // HTTPS-only in production (cfg.IsProd())
SameSite: http.SameSiteLaxMode, // sent on top-level navigations, not cross-site POSTs
Create is used this lesson (register); User and Destroy are the machinery lesson 5 will call for login and logout — written now because they're one cohesive unit.