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
}