package authn import ( "encoding/json" "log" "net/http" "git.sr.ht/~sircmpwn/core-go/auth" "git.sr.ht/~sircmpwn/core-go/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 }