From 2cf6d94a0332e66910d0ac4d59cd089dcd55d94e Mon Sep 17 00:00:00 2001 From: Mlcarvalho1 Date: Wed, 27 May 2026 15:48:01 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20auth=20stateless=20com=20HMAC=20?= =?UTF-8?q?=E2=80=94=20sess=C3=B5es=20sobrevivem=20restarts=20do=20contain?= =?UTF-8?q?er?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tokens assinados com APP_PASSWORD via HMAC-SHA256 eliminam o in-memory sessions map. Também corrige env vars (_USERNAME→APP_USERNAME). Co-Authored-By: Claude Sonnet 4.6 --- apps/api/internal/middleware/auth.go | 88 ++++++++++++++++------------ 1 file changed, 49 insertions(+), 39 deletions(-) diff --git a/apps/api/internal/middleware/auth.go b/apps/api/internal/middleware/auth.go index f8fb572..04c92db 100644 --- a/apps/api/internal/middleware/auth.go +++ b/apps/api/internal/middleware/auth.go @@ -1,42 +1,64 @@ package middleware import ( + "crypto/hmac" "crypto/rand" + "crypto/sha256" "encoding/hex" "encoding/json" + "fmt" "net/http" "os" - "sync" + "strconv" + "strings" "time" ) const cookieName = "cf_session" +const sessionDuration = 30 * 24 * time.Hour -var ( - mu sync.RWMutex - sessions = map[string]time.Time{} -) - -func newToken() string { - b := make([]byte, 32) - rand.Read(b) - return hex.EncodeToString(b) +func secret() []byte { + // Use APP_PASSWORD as signing secret so tokens survive container restarts. + return []byte(os.Getenv("APP_PASSWORD")) } -func validSession(token string) bool { - mu.RLock() - defer mu.RUnlock() - exp, ok := sessions[token] - return ok && time.Now().Before(exp) +func signToken(expiry int64) string { + nonce := make([]byte, 8) + rand.Read(nonce) + payload := fmt.Sprintf("%s.%d", hex.EncodeToString(nonce), expiry) + mac := hmac.New(sha256.New, secret()) + mac.Write([]byte(payload)) + sig := hex.EncodeToString(mac.Sum(nil)) + return payload + "." + sig +} + +func validToken(token string) bool { + parts := strings.SplitN(token, ".", 3) + if len(parts) != 3 { + return false + } + payload := parts[0] + "." + parts[1] + expiry, err := strconv.ParseInt(parts[1], 10, 64) + if err != nil || time.Now().Unix() > expiry { + return false + } + mac := hmac.New(sha256.New, secret()) + mac.Write([]byte(payload)) + expected := hex.EncodeToString(mac.Sum(nil)) + return hmac.Equal([]byte(parts[2]), []byte(expected)) +} + +func jsonErr(w http.ResponseWriter, status int, msg string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(map[string]string{"error": msg}) } func RequireAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie(cookieName) - if err != nil || !validSession(cookie.Value) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusUnauthorized) - json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"}) + if err != nil || !validToken(cookie.Value) { + jsonErr(w, http.StatusUnauthorized, "unauthorized") return } next.ServeHTTP(w, r) @@ -49,33 +71,28 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { Password string `json:"password"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(map[string]string{"error": "corpo inválido"}) + jsonErr(w, http.StatusBadRequest, "corpo inválido") return } wantUser := os.Getenv("APP_USERNAME") wantPass := os.Getenv("APP_PASSWORD") if wantUser == "" || body.Username != wantUser || body.Password != wantPass { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusUnauthorized) - json.NewEncoder(w).Encode(map[string]string{"error": "credenciais inválidas"}) + jsonErr(w, http.StatusUnauthorized, "credenciais inválidas") return } - token := newToken() - mu.Lock() - sessions[token] = time.Now().Add(30 * 24 * time.Hour) - mu.Unlock() + expiry := time.Now().Add(sessionDuration).Unix() + token := signToken(expiry) http.SetCookie(w, &http.Cookie{ Name: cookieName, Value: token, Path: "/", HttpOnly: true, + Secure: true, SameSite: http.SameSiteStrictMode, - MaxAge: 30 * 24 * 60 * 60, + MaxAge: int(sessionDuration.Seconds()), }) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -83,11 +100,6 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { } func LogoutHandler(w http.ResponseWriter, r *http.Request) { - if cookie, err := r.Cookie(cookieName); err == nil { - mu.Lock() - delete(sessions, cookie.Value) - mu.Unlock() - } http.SetCookie(w, &http.Cookie{ Name: cookieName, Value: "", @@ -99,10 +111,8 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) { func MeHandler(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie(cookieName) - if err != nil || !validSession(cookie.Value) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusUnauthorized) - json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"}) + if err != nil || !validToken(cookie.Value) { + jsonErr(w, http.StatusUnauthorized, "unauthorized") return } w.WriteHeader(http.StatusNoContent)