diff --git a/backend/main.go b/backend/main.go index 00057bd1..04c8757e 100644 --- a/backend/main.go +++ b/backend/main.go @@ -99,19 +99,31 @@ func main() { limiter := middleware.NewRateLimiter(30*time.Second, 50) rateLimitedHandler := middleware.RateLimitMiddleware(limiter) + // Auth middleware validates session and injects session credentials into request body + // This prevents credential manipulation and allows frontend to not store sensitive secrets + authHandler := middleware.AuthMiddleware(store) + + // Helper to compose rate limiting + auth middleware + authenticatedHandler := func(h http.Handler) http.Handler { + return rateLimitedHandler(authHandler(h)) + } + + // Auth endpoints (no auth middleware - these handle authentication) mux.Handle("/auth/oauth", rateLimitedHandler(http.HandlerFunc(app.OAuthHandler))) mux.Handle("/auth/callback", rateLimitedHandler(http.HandlerFunc(app.OAuthCallbackHandler))) mux.Handle("/api/user", rateLimitedHandler(http.HandlerFunc(app.UserInfoHandler))) mux.Handle("/auth/logout", rateLimitedHandler(http.HandlerFunc(app.LogoutHandler))) - mux.Handle("/tasks", rateLimitedHandler(http.HandlerFunc(controllers.TasksHandler))) - mux.Handle("/add-task", rateLimitedHandler(http.HandlerFunc(controllers.AddTaskHandler))) - mux.Handle("/edit-task", rateLimitedHandler(http.HandlerFunc(controllers.EditTaskHandler))) - mux.Handle("/modify-task", rateLimitedHandler(http.HandlerFunc(controllers.ModifyTaskHandler))) - mux.Handle("/complete-task", rateLimitedHandler(http.HandlerFunc(controllers.CompleteTaskHandler))) - mux.Handle("/delete-task", rateLimitedHandler(http.HandlerFunc(controllers.DeleteTaskHandler))) + + // Task endpoints - require authentication, credentials injected from session + mux.Handle("/tasks", authenticatedHandler(http.HandlerFunc(controllers.TasksHandler))) + mux.Handle("/add-task", authenticatedHandler(http.HandlerFunc(controllers.AddTaskHandler))) + mux.Handle("/edit-task", authenticatedHandler(http.HandlerFunc(controllers.EditTaskHandler))) + mux.Handle("/modify-task", authenticatedHandler(http.HandlerFunc(controllers.ModifyTaskHandler))) + mux.Handle("/complete-task", authenticatedHandler(http.HandlerFunc(controllers.CompleteTaskHandler))) + mux.Handle("/delete-task", authenticatedHandler(http.HandlerFunc(controllers.DeleteTaskHandler))) mux.Handle("/sync/logs", rateLimitedHandler(http.HandlerFunc(controllers.SyncLogsHandler))) - mux.Handle("/complete-tasks", rateLimitedHandler(http.HandlerFunc(controllers.BulkCompleteTaskHandler))) - mux.Handle("/delete-tasks", rateLimitedHandler(http.HandlerFunc(controllers.BulkDeleteTaskHandler))) + mux.Handle("/complete-tasks", authenticatedHandler(http.HandlerFunc(controllers.BulkCompleteTaskHandler))) + mux.Handle("/delete-tasks", authenticatedHandler(http.HandlerFunc(controllers.BulkDeleteTaskHandler))) mux.HandleFunc("/health", controllers.HealthCheckHandler) diff --git a/backend/middleware/auth.go b/backend/middleware/auth.go new file mode 100644 index 00000000..8349fe69 --- /dev/null +++ b/backend/middleware/auth.go @@ -0,0 +1,89 @@ +package middleware + +import ( + "bytes" + "ccsync_backend/utils" + "encoding/json" + "io" + "net/http" + + "github.com/gorilla/sessions" +) + +// credentialsPayload represents the common credential fields in request bodies +type credentialsPayload struct { + Email string `json:"email"` + EncryptionSecret string `json:"encryptionSecret"` + UUID string `json:"UUID"` +} + +// AuthMiddleware validates that the user is authenticated and injects session +// credentials into the request body. This ensures: +// 1. Only authenticated users can access protected endpoints +// 2. Users cannot manipulate credentials to access other users' data +// 3. Frontend doesn't need to store/send sensitive encryption secrets +func AuthMiddleware(store *sessions.CookieStore) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Get session + session, err := store.Get(r, "session-name") + if err != nil { + utils.Logger.Warnf("Auth middleware: failed to get session: %v", err) + http.Error(w, "Authentication required", http.StatusUnauthorized) + return + } + + // Check if user is authenticated + userInfo, ok := session.Values["user"].(map[string]interface{}) + if !ok || userInfo == nil { + http.Error(w, "Authentication required", http.StatusUnauthorized) + return + } + + // Extract session credentials + sessionEmail, _ := userInfo["email"].(string) + sessionUUID, _ := userInfo["uuid"].(string) + sessionSecret, _ := userInfo["encryption_secret"].(string) + + if sessionEmail == "" || sessionUUID == "" || sessionSecret == "" { + utils.Logger.Warnf("Auth middleware: incomplete session credentials") + http.Error(w, "Authentication required", http.StatusUnauthorized) + return + } + + // For POST requests with JSON body, inject session credentials + if r.Method == http.MethodPost && r.Body != nil { + // Read the body + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusBadRequest) + return + } + + // Parse the body as a generic map + var bodyMap map[string]interface{} + if err := json.Unmarshal(bodyBytes, &bodyMap); err == nil { + // Inject/overwrite credentials from session + // This ensures users can't manipulate credentials to access other users' data + bodyMap["email"] = sessionEmail + bodyMap["UUID"] = sessionUUID + bodyMap["encryptionSecret"] = sessionSecret + + // Re-encode the modified body + modifiedBody, err := json.Marshal(bodyMap) + if err != nil { + http.Error(w, "Failed to process request body", http.StatusInternalServerError) + return + } + r.Body = io.NopCloser(bytes.NewBuffer(modifiedBody)) + r.ContentLength = int64(len(modifiedBody)) + } else { + // If we can't parse the body, restore original and let handler deal with it + r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + } + } + + next.ServeHTTP(w, r) + }) + } +}