Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
89 changes: 89 additions & 0 deletions backend/middleware/auth.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
Loading