package auth import ( "net/http" "time" ) // DevCookieName is the session cookie's name when Secure cannot be set. It is a DIFFERENT name on // purpose: __Host- is not decoration a browser can be talked out of — a cookie with that prefix and // no Secure attribute is simply rejected — so a "local development" profile that kept the name // would fail in a way that looks like a broken login (PD-8). const DevCookieName = "tm_session" // LoginCookieName holds the OAuth state while the browser is away at the provider. Short-lived, // single-use, and paired with a server-side row: the cookie proves the callback came back to the // same browser that started, which is what stops a login-CSRF. const ( LoginCookieName = "__Host-tm_login" DevLoginCookieName = "tm_login" ) // Cookies writes the browser's credentials. One switch, and it flips the name with the attributes. type Cookies struct { // Insecure serves plain HTTP: no Secure attribute, no __Host- prefix. Production never sets it. Insecure bool } func (c Cookies) SessionName() string { if c.Insecure { return DevCookieName } return CookieName } func (c Cookies) LoginName() string { if c.Insecure { return DevLoginCookieName } return LoginCookieName } // SetSession writes the session cookie. // // SameSite=Lax rather than Strict: the browser returns from the identity provider by a top-level // GET, and Strict would withhold the cookie on every arrival from an external link. Lax still // withholds it from cross-site POSTs, and the CSRF layer covers the rest. func (c Cookies) SetSession(w http.ResponseWriter, token string, ttl time.Duration) { c.set(w, c.SessionName(), token, ttl) } // ClearSession removes it. Attributes must match the ones it was set with or the browser keeps it. func (c Cookies) ClearSession(w http.ResponseWriter) { c.set(w, c.SessionName(), "", -time.Second) } func (c Cookies) SetLogin(w http.ResponseWriter, state string, ttl time.Duration) { c.set(w, c.LoginName(), state, ttl) } // ClearLogin removes it. The callback clears it whether it succeeded or not: a state cookie that // outlives its round trip is a replay waiting for an accident. func (c Cookies) ClearLogin(w http.ResponseWriter) { c.set(w, c.LoginName(), "", -time.Second) } func (c Cookies) set(w http.ResponseWriter, name, value string, ttl time.Duration) { maxAge := int(ttl.Seconds()) if ttl < 0 { maxAge = -1 } http.SetCookie(w, &http.Cookie{ Name: name, Value: value, Path: "/", MaxAge: maxAge, HttpOnly: true, Secure: !c.Insecure, SameSite: http.SameSiteLaxMode, }) }