package authn import ( "log/slog" "net/http" "go.bigb.es/auxilia/scribe" "sourcecraft.dev/bigbes/sr-ht-core/auth" "sourcecraft.dev/bigbes/sr-ht-ecore/login" ) // OptionalCookieMiddleware reads the unified-login cookie and, when it names a // user this service can resolve, attaches the resolved caller to the request // context (retrievable with CallerFromContext). It NEVER rejects a request: a // missing, malformed, undecryptable, or unresolvable cookie leaves the request // anonymous. This is what allows public browsing and public clones to work // without credentials — unlike core-go's auth.Middleware, which 401s any // request lacking a cookie or Authorization header. // // The two halves of that sentence are two packages, and the split is the point. // Decoding the cookie is instance-wide — one session, one seal, one grammar for // the name inside it — and lives in sr-ht-ecore's login. Turning the name into a // row is dolt.sr.ht's alone: our user table, our mirror-on-first-sight, our // answer for a user meta has but we have never seen. Only the second half is // here, which is also why this is not simply login.Optional: what the rest of // the service reads out of the context is an *auth.AuthContext with a UserID, // not a username. // // Requires crypto.InitCrypto to have run (server.New does this at startup) and, // for the user lookup, config.Middleware + database.Middleware installed // upstream so the context carries the config and database. func OptionalCookieMiddleware() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if ac := resolveCookie(r); ac != nil { r = r.WithContext(WithCaller(r.Context(), ac)) } next.ServeHTTP(w, r) }) } } // resolveCookie returns the caller authenticated by the request's unified-login // cookie, or nil if there is no cookie or it cannot be resolved for any reason. // Every failure path returns nil (anonymous) — none is fatal. // // Suspended users are resolved normally; the suspension flag is carried on the // caller (via AsCoreCaller) and gates writes at the access-control layer rather // than being rejected here. func resolveCookie(r *http.Request) *auth.AuthContext { // No cookie, a forged one, a payload that is not core-go's JSON, or a name // that could not be an account name: all "" and all anonymous. Nothing is // logged, because the cookie value is attacker-supplied and arrives on every // request — a warning per bad decode is a log flood anyone can turn on. username := login.UsernameFromRequest(r) if username == "" { return nil } var ac auth.AuthContext if err := meta.LookupUser(r.Context(), username, &ac); err != nil { // meta/database unreachable or unknown user: degrade to anonymous // rather than failing the request (browsing must keep working). This // one *is* logged: the name has already passed login's grammar, so it // is bounded text, and an unreachable meta is an operator's problem. slog.WarnContext(r.Context(), "resolving the login cookie's user failed", "component", "authn", "username", username, scribe.Err(err)) return nil } ac.AuthMethod = auth.AUTH_COOKIE return &ac }