package auth import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "fmt" "net/http" "strconv" "strings" "time" "github.com/google/uuid" ) const defaultTTL = 7 * 24 * time.Hour type Session struct { secret []byte cookieName string path string ttl time.Duration } func NewSession(secret, cookieName, path string) *Session { return &Session{ secret: []byte(secret), cookieName: cookieName, path: path, ttl: defaultTTL, } } func (s *Session) Create(value string) (string, error) { exp := time.Now().Add(s.ttl).Unix() payload := fmt.Sprintf("%s|%d", value, exp) sig := s.sign(payload) return base64.URLEncoding.EncodeToString([]byte(payload + "|" + sig)), nil } func (s *Session) Validate(token string) (string, bool) { raw, err := base64.URLEncoding.DecodeString(token) if err != nil { return "", false } parts := strings.Split(string(raw), "|") if len(parts) != 3 { return "", false } value, expStr, sig := parts[0], parts[1], parts[2] payload := value + "|" + expStr if !hmac.Equal([]byte(sig), []byte(s.sign(payload))) { return "", false } exp, err := strconv.ParseInt(expStr, 10, 64) if err != nil || time.Now().Unix() > exp { return "", false } return value, true } func (s *Session) SetCookie(w http.ResponseWriter, r *http.Request, token string) { http.SetCookie(w, &http.Cookie{ Name: s.cookieName, Value: token, Path: s.path, HttpOnly: true, Secure: isSecure(r), SameSite: http.SameSiteLaxMode, MaxAge: int(s.ttl.Seconds()), }) } func (s *Session) ClearCookie(w http.ResponseWriter, r *http.Request) { http.SetCookie(w, &http.Cookie{ Name: s.cookieName, Value: "", Path: s.path, HttpOnly: true, Secure: isSecure(r), MaxAge: -1, }) } func (s *Session) FromRequest(r *http.Request) (string, bool) { c, err := r.Cookie(s.cookieName) if err != nil { return "", false } return s.Validate(c.Value) } func (s *Session) EnsureKey(w http.ResponseWriter, r *http.Request) string { if key, ok := s.FromRequest(r); ok && key != "" { return key } key := uuid.New().String() token, _ := s.Create(key) s.SetCookie(w, r, token) return key } func (s *Session) sign(payload string) string { mac := hmac.New(sha256.New, s.secret) mac.Write([]byte(payload)) return base64.URLEncoding.EncodeToString(mac.Sum(nil)) } func isSecure(r *http.Request) bool { if r.TLS != nil { return true } return strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") } func UserIDFromSession(s *Session, r *http.Request) (int, bool) { val, ok := s.FromRequest(r) if !ok { return 0, false } if !strings.HasPrefix(val, "user:") { return 0, false } id, err := strconv.Atoi(strings.TrimPrefix(val, "user:")) return id, err == nil && id > 0 } func SetUserSession(s *Session, w http.ResponseWriter, r *http.Request, userID int) error { token, err := s.Create(fmt.Sprintf("user:%d", userID)) if err != nil { return err } s.SetCookie(w, r, token) return nil }