package authn import ( "encoding/json" "net/http" "strings" "sourcecraft.dev/bigbes/sr-ht-core/auth" "sourcecraft.dev/bigbes/sr-ht-core/crypto" "sourcecraft.dev/bigbes/sr-ht-spec/core" ) // CookieName is the unified-login cookie shared by every service on the // instance. Its value is a Fernet token sealed by meta.sr.ht with the // [sr.ht] network-key, which is why spec.srht.bigb.es must live under the // shared *.srht.bigb.es cookie domain — otherwise the cookie is never sent to // us and every viewer looks anonymous. const CookieName = "sr.ht.unified-login.v1" // UsernameFromRequest returns the username carried by the unified-login cookie, // or "" for an anonymous viewer. // // Every failure path — no cookie, forged or truncated ciphertext, a payload // that is not JSON, a payload with no name — returns "" rather than an error. // This service never serves an error page on identity grounds; it decides what // an anonymous viewer may see instead. Returning an error here would turn a // stale cookie from a browser tab left open over a key rotation into a broken // site rather than a logged-out one. // // Requires crypto.InitCrypto to have run (server.New does it at startup). func UsernameFromRequest(r *http.Request) string { c, err := r.Cookie(CookieName) if err != nil { return "" // no cookie: anonymous } return UsernameFromCookie(c.Value) } // UsernameFromCookie is UsernameFromRequest for a cookie value already in hand // — the form the hooks' RPC path and tests need, where there is no // *http.Request to read from. // // Note the deliberate use of DecryptWithoutExpiration, matching core-go's own // cookieAuth: the unified-login cookie carries no service-side TTL, and its // lifetime is the browser cookie's Expires plus meta.sr.ht's ability to rotate // the network key. Adding a TTL here would log the owner out of this one // service on a schedule no other service shares. func UsernameFromCookie(value string) string { if value == "" { return "" } payload := crypto.DecryptWithoutExpiration([]byte(value)) if payload == nil { return "" // forged, tampered, or sealed with a key we no longer hold } var claims auth.AuthCookie if err := json.Unmarshal(payload, &claims); err != nil { return "" // well-sealed but malformed payload } // Cookies carry the bare username; strip a leading '~' defensively in case // something upstream stored the canonical "~user" form. name := strings.TrimPrefix(claims.Name, "~") if err := core.ValidateOwner(name); err != nil { // An unusable username is not an identity. Rejecting here keeps a // hostile cookie payload out of path construction and log lines. return "" } return name }