Artifact 522cc14de075e7c51528b3cc84f77d4c25a910fd8d66fff4949476b097483203:
- File
internal/web/session_test.go
— part of check-in
[7bdf7a189b]
at
2026-08-17 00:10:56
on branch trunk
— web: cookie sessions and a login page
Basic auth works and stays for the API, but a browser has no way to log out of it and the credential lives in the browser's keeping. A gated page now redirects to /login instead of raising a Basic prompt, the header names who is signed in and offers sign-out, and Basic remains for scripts.
Session tokens are stored as SHA-256 digests, so the table cannot be read back into working cookies. Cookies are HttpOnly, Path=/, SameSite=Lax — Lax being what stops another origin POSTing quarry's write forms on a reader's behalf — and Secure under TLS.
A password change or an account deletion ends that account's sessions; otherwise 'change your password' would leave the browser already holding one still signed in. Authorization for a cookie runs the same RoleOf lookup the password path uses, so a cookie is never worth more than a password.
safeNext keeps the post-login redirect on this site: a reader is at their most trusting on the hop straight after authenticating, which is exactly when an open redirect pays. (user: dmestas, size: 991) [annotate] [blame] [check-ins using]
package web import "testing" // safeNext guards the post-login redirect. Anyone who can choose ?next= // can otherwise land a freshly-signed-in reader on their own page, which // is how a login form becomes an open redirect — and the reader is at // their most trusting on the hop straight after authenticating. func TestSafeNextRefusesOffSiteTargets(t *testing.T) { for _, bad := range []string{ "https://evil.example/steal", "//evil.example/steal", // scheme-relative: a browser treats this as off-site "http://evil.example", "/ok\nSet-Cookie: x=1", // header injection through the redirect "/ok\r\nLocation: https://evil.example", "javascript:alert(1)", "", } { if got := safeNext(bad); got != "/" { t.Errorf("safeNext(%q) = %q, want \"/\"", bad, got) } } for _, ok := range []string{ "/r/alpha/wiki/Home", "/r/alpha/ticket/abc/edit", "/", } { if got := safeNext(ok); got != ok { t.Errorf("safeNext(%q) = %q, want it unchanged", ok, got) } } }