76 lines
2.5 KiB
Go
76 lines
2.5 KiB
Go
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,
|
|
})
|
|
}
|