package authn
import (
"encoding/json"
"log"
"net/http"
"sourcecraft.dev/bigbes/sr-ht-core/auth"
"sourcecraft.dev/bigbes/sr-ht-core/crypto"
)
// CookieName is the unified-login cookie shared across every SourceHut service.
const CookieName = "sr.ht.unified-login.v1"
// OptionalCookieMiddleware reads the sr.ht.unified-login.v1 cookie and, when it
// is present and valid, 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.
//
// It replicates the logic of core-go's unexported cookieAuth: decrypt the
// fernet-sealed cookie with crypto.DecryptWithoutExpiration, decode the
// {"name": ...} payload, and resolve the user via the meta backend (core-go's
// auth.LookupUser path). 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.
//
// 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.
func resolveCookie(r *http.Request) *auth.AuthContext {
cookie, err := r.Cookie(CookieName)
if err != nil {
return nil // no cookie: anonymous
}
payload := crypto.DecryptWithoutExpiration([]byte(cookie.Value))
if payload == nil {
return nil // bad/forged/undecryptable cookie: anonymous
}
var authCookie auth.AuthCookie
if err := json.Unmarshal(payload, &authCookie); err != nil {
return nil // malformed payload: anonymous
}
if authCookie.Name == "" {
return nil // no username in payload: anonymous
}
var ac auth.AuthContext
if err := meta.LookupUser(r.Context(), authCookie.Name, &ac); err != nil {
// meta/database unreachable or unknown user: degrade to anonymous
// rather than failing the request (browsing must keep working).
log.Printf("authn: cookie LookupUser(%q): %v", authCookie.Name, err)
return nil
}
ac.AuthMethod = auth.AUTH_COOKIE
return &ac
}