package authz import ( "context" "encoding/json" "net/http" "strings" "git.sr.ht/~sircmpwn/core-go/crypto" ) // CookieName is the SourceHut unified-login session cookie. Its value is a // Fernet token encrypted with the instance [sr.ht] network-key. const CookieName = "sr.ht.unified-login.v1" // UsernameFromRequest extracts the authenticated username from the unified-login // cookie, or returns "" for an anonymous viewer. Any failure — missing cookie, // undecryptable token, malformed JSON — is treated as anonymous rather than an // error: this service never rejects a request on identity grounds, it only lets // git.sr.ht decide what an anonymous viewer may see. func UsernameFromRequest(r *http.Request) string { c, err := r.Cookie(CookieName) if err != nil { return "" } // InitCrypto must have run (server.New does it); the network-key here is // the same Fernet key meta.sr.ht used to seal the cookie. payload := crypto.DecryptWithoutExpiration([]byte(c.Value)) if payload == nil { return "" } var claims struct { Name string `json:"name"` } if err := json.Unmarshal(payload, &claims); err != nil { return "" } // Cookies carry the bare username; strip a leading "~" defensively in case // a caller stored the canonical "~user" form. return strings.TrimPrefix(claims.Name, "~") } type ctxKey int const usernameKey ctxKey = iota // Middleware stores the cookie-derived username in the request context. It // never writes a 401: an anonymous viewer flows through with an empty username // and git.sr.ht enforces visibility downstream. func Middleware() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := context.WithValue(r.Context(), usernameKey, UsernameFromRequest(r)) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // ForContext returns the username stored by Middleware, or "" if absent. func ForContext(ctx context.Context) string { username, _ := ctx.Value(usernameKey).(string) return username }