coverage~bigbes/sr-ht-dolt3523280cauthn/cookie.go

Coverage
100.0% 14/14 statements
Δ
+0.0
Blob
0ee82e3
Uncovered nothing — every instrumented line ran
1 package authn
2
3 import (
4 "log/slog"
5 "net/http"
6
7 "go.bigb.es/auxilia/scribe"
8
9 "sourcecraft.dev/bigbes/sr-ht-core/auth"
10
11 "sourcecraft.dev/bigbes/sr-ht-ecore/login"
12 )
13
14 // OptionalCookieMiddleware reads the unified-login cookie and, when it names a
15 // user this service can resolve, attaches the resolved caller to the request
16 // context (retrievable with CallerFromContext). It NEVER rejects a request: a
17 // missing, malformed, undecryptable, or unresolvable cookie leaves the request
18 // anonymous. This is what allows public browsing and public clones to work
19 // without credentials — unlike core-go's auth.Middleware, which 401s any
20 // request lacking a cookie or Authorization header.
21 //
22 // The two halves of that sentence are two packages, and the split is the point.
23 // Decoding the cookie is instance-wide — one session, one seal, one grammar for
24 // the name inside it — and lives in sr-ht-ecore's login. Turning the name into a
25 // row is dolt.sr.ht's alone: our user table, our mirror-on-first-sight, our
26 // answer for a user meta has but we have never seen. Only the second half is
27 // here, which is also why this is not simply login.Optional: what the rest of
28 // the service reads out of the context is an *auth.AuthContext with a UserID,
29 // not a username.
30 //
31 // Requires crypto.InitCrypto to have run (server.New does this at startup) and,
32 // for the user lookup, config.Middleware + database.Middleware installed
33 // upstream so the context carries the config and database.
34 10 func OptionalCookieMiddleware() func(http.Handler) http.Handler {
35 10 return func(next http.Handler) http.Handler {
36 10 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
37 10 if ac := resolveCookie(r); ac != nil {
38 3 r = r.WithContext(WithCaller(r.Context(), ac))
39 3 }
40 10 next.ServeHTTP(w, r)
41 })
42 }
43 }
44
45 // resolveCookie returns the caller authenticated by the request's unified-login
46 // cookie, or nil if there is no cookie or it cannot be resolved for any reason.
47 // Every failure path returns nil (anonymous) — none is fatal.
48 //
49 // Suspended users are resolved normally; the suspension flag is carried on the
50 // caller (via AsCoreCaller) and gates writes at the access-control layer rather
51 // than being rejected here.
52 10 func resolveCookie(r *http.Request) *auth.AuthContext {
53 10 // No cookie, a forged one, a payload that is not core-go's JSON, or a name
54 10 // that could not be an account name: all "" and all anonymous. Nothing is
55 10 // logged, because the cookie value is attacker-supplied and arrives on every
56 10 // request — a warning per bad decode is a log flood anyone can turn on.
57 10 username := login.UsernameFromRequest(r)
58 10 if username == "" {
59 5 return nil
60 5 }
61
62 5 var ac auth.AuthContext
63 5 if err := meta.LookupUser(r.Context(), username, &ac); err != nil {
64 2 // meta/database unreachable or unknown user: degrade to anonymous
65 2 // rather than failing the request (browsing must keep working). This
66 2 // one *is* logged: the name has already passed login's grammar, so it
67 2 // is bounded text, and an unreachable meta is an operator's problem.
68 2 slog.WarnContext(r.Context(), "resolving the login cookie's user failed",
69 2 "component", "authn", "username", username, scribe.Err(err))
70 2 return nil
71 2 }
72 3 ac.AuthMethod = auth.AUTH_COOKIE
73 3 return &ac
74 }