package main import ( "crypto/hmac" "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/json" "net/http" "strings" "time" ) const ( cookieName = "docent_session" cookieMaxAge = 30 * 24 * time.Hour ) type session struct { Exp int64 `json:"exp"` } func (s *server) signCookieValue() (string, error) { payload := session{Exp: time.Now().Add(cookieMaxAge).Unix()} data, err := json.Marshal(payload) if err != nil { return "", err } enc := base64.RawURLEncoding.EncodeToString(data) mac := hmac.New(sha256.New, s.secret) mac.Write([]byte(enc)) sig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) return enc + "." + sig, nil } func (s *server) verifyCookieValue(value string) bool { parts := strings.SplitN(value, ".", 2) if len(parts) != 2 { return false } mac := hmac.New(sha256.New, s.secret) mac.Write([]byte(parts[0])) expected := base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(expected), []byte(parts[1])) { return false } data, err := base64.RawURLEncoding.DecodeString(parts[0]) if err != nil { return false } var sess session if err := json.Unmarshal(data, &sess); err != nil { return false } return time.Now().Unix() < sess.Exp } func (s *server) setSessionCookie(w http.ResponseWriter) { value, err := s.signCookieValue() if err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } http.SetCookie(w, &http.Cookie{ Name: cookieName, Value: value, Path: "/", MaxAge: int(cookieMaxAge.Seconds()), HttpOnly: true, Secure: s.secure, SameSite: http.SameSiteLaxMode, }) } func (s *server) clearSessionCookie(w http.ResponseWriter) { http.SetCookie(w, &http.Cookie{ Name: cookieName, Value: "", Path: "/", MaxAge: -1, HttpOnly: true, Secure: s.secure, SameSite: http.SameSiteLaxMode, }) } func (s *server) isAuthed(r *http.Request) bool { c, err := r.Cookie(cookieName) if err != nil { return false } return s.verifyCookieValue(c.Value) } func constantTimeEqualString(a, b string) bool { return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1 }