Skip to content

Commit 5106518

Browse files
cfsmp3claude
andauthored
security: inject session credentials into API requests (#408)
Add authentication middleware that injects credentials from session into request body, preventing credential manipulation attacks. This approach: - Validates user has an authenticated session - Overwrites any provided credentials with session credentials - Prevents users from manipulating credentials to access other data - Makes frontend credential storage optional (still shown in setup guide) Note: Encryption secret is still shown in SetupGuide for users to configure their local taskwarrior CLI, but is no longer trusted from API requests. Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 826b596 commit 5106518

2 files changed

Lines changed: 109 additions & 8 deletions

File tree

backend/main.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -99,19 +99,31 @@ func main() {
9999
limiter := middleware.NewRateLimiter(30*time.Second, 50)
100100
rateLimitedHandler := middleware.RateLimitMiddleware(limiter)
101101

102+
// Auth middleware validates session and injects session credentials into request body
103+
// This prevents credential manipulation and allows frontend to not store sensitive secrets
104+
authHandler := middleware.AuthMiddleware(store)
105+
106+
// Helper to compose rate limiting + auth middleware
107+
authenticatedHandler := func(h http.Handler) http.Handler {
108+
return rateLimitedHandler(authHandler(h))
109+
}
110+
111+
// Auth endpoints (no auth middleware - these handle authentication)
102112
mux.Handle("/auth/oauth", rateLimitedHandler(http.HandlerFunc(app.OAuthHandler)))
103113
mux.Handle("/auth/callback", rateLimitedHandler(http.HandlerFunc(app.OAuthCallbackHandler)))
104114
mux.Handle("/api/user", rateLimitedHandler(http.HandlerFunc(app.UserInfoHandler)))
105115
mux.Handle("/auth/logout", rateLimitedHandler(http.HandlerFunc(app.LogoutHandler)))
106-
mux.Handle("/tasks", rateLimitedHandler(http.HandlerFunc(controllers.TasksHandler)))
107-
mux.Handle("/add-task", rateLimitedHandler(http.HandlerFunc(controllers.AddTaskHandler)))
108-
mux.Handle("/edit-task", rateLimitedHandler(http.HandlerFunc(controllers.EditTaskHandler)))
109-
mux.Handle("/modify-task", rateLimitedHandler(http.HandlerFunc(controllers.ModifyTaskHandler)))
110-
mux.Handle("/complete-task", rateLimitedHandler(http.HandlerFunc(controllers.CompleteTaskHandler)))
111-
mux.Handle("/delete-task", rateLimitedHandler(http.HandlerFunc(controllers.DeleteTaskHandler)))
116+
117+
// Task endpoints - require authentication, credentials injected from session
118+
mux.Handle("/tasks", authenticatedHandler(http.HandlerFunc(controllers.TasksHandler)))
119+
mux.Handle("/add-task", authenticatedHandler(http.HandlerFunc(controllers.AddTaskHandler)))
120+
mux.Handle("/edit-task", authenticatedHandler(http.HandlerFunc(controllers.EditTaskHandler)))
121+
mux.Handle("/modify-task", authenticatedHandler(http.HandlerFunc(controllers.ModifyTaskHandler)))
122+
mux.Handle("/complete-task", authenticatedHandler(http.HandlerFunc(controllers.CompleteTaskHandler)))
123+
mux.Handle("/delete-task", authenticatedHandler(http.HandlerFunc(controllers.DeleteTaskHandler)))
112124
mux.Handle("/sync/logs", rateLimitedHandler(controllers.SyncLogsHandler(store)))
113-
mux.Handle("/complete-tasks", rateLimitedHandler(http.HandlerFunc(controllers.BulkCompleteTaskHandler)))
114-
mux.Handle("/delete-tasks", rateLimitedHandler(http.HandlerFunc(controllers.BulkDeleteTaskHandler)))
125+
mux.Handle("/complete-tasks", authenticatedHandler(http.HandlerFunc(controllers.BulkCompleteTaskHandler)))
126+
mux.Handle("/delete-tasks", authenticatedHandler(http.HandlerFunc(controllers.BulkDeleteTaskHandler)))
115127

116128
mux.HandleFunc("/health", controllers.HealthCheckHandler)
117129

backend/middleware/auth.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package middleware
2+
3+
import (
4+
"bytes"
5+
"ccsync_backend/utils"
6+
"encoding/json"
7+
"io"
8+
"net/http"
9+
10+
"github.com/gorilla/sessions"
11+
)
12+
13+
// credentialsPayload represents the common credential fields in request bodies
14+
type credentialsPayload struct {
15+
Email string `json:"email"`
16+
EncryptionSecret string `json:"encryptionSecret"`
17+
UUID string `json:"UUID"`
18+
}
19+
20+
// AuthMiddleware validates that the user is authenticated and injects session
21+
// credentials into the request body. This ensures:
22+
// 1. Only authenticated users can access protected endpoints
23+
// 2. Users cannot manipulate credentials to access other users' data
24+
// 3. Frontend doesn't need to store/send sensitive encryption secrets
25+
func AuthMiddleware(store *sessions.CookieStore) func(http.Handler) http.Handler {
26+
return func(next http.Handler) http.Handler {
27+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
28+
// Get session
29+
session, err := store.Get(r, "session-name")
30+
if err != nil {
31+
utils.Logger.Warnf("Auth middleware: failed to get session: %v", err)
32+
http.Error(w, "Authentication required", http.StatusUnauthorized)
33+
return
34+
}
35+
36+
// Check if user is authenticated
37+
userInfo, ok := session.Values["user"].(map[string]interface{})
38+
if !ok || userInfo == nil {
39+
http.Error(w, "Authentication required", http.StatusUnauthorized)
40+
return
41+
}
42+
43+
// Extract session credentials
44+
sessionEmail, _ := userInfo["email"].(string)
45+
sessionUUID, _ := userInfo["uuid"].(string)
46+
sessionSecret, _ := userInfo["encryption_secret"].(string)
47+
48+
if sessionEmail == "" || sessionUUID == "" || sessionSecret == "" {
49+
utils.Logger.Warnf("Auth middleware: incomplete session credentials")
50+
http.Error(w, "Authentication required", http.StatusUnauthorized)
51+
return
52+
}
53+
54+
// For POST requests with JSON body, inject session credentials
55+
if r.Method == http.MethodPost && r.Body != nil {
56+
// Read the body
57+
bodyBytes, err := io.ReadAll(r.Body)
58+
if err != nil {
59+
http.Error(w, "Failed to read request body", http.StatusBadRequest)
60+
return
61+
}
62+
63+
// Parse the body as a generic map
64+
var bodyMap map[string]interface{}
65+
if err := json.Unmarshal(bodyBytes, &bodyMap); err == nil {
66+
// Inject/overwrite credentials from session
67+
// This ensures users can't manipulate credentials to access other users' data
68+
bodyMap["email"] = sessionEmail
69+
bodyMap["UUID"] = sessionUUID
70+
bodyMap["encryptionSecret"] = sessionSecret
71+
72+
// Re-encode the modified body
73+
modifiedBody, err := json.Marshal(bodyMap)
74+
if err != nil {
75+
http.Error(w, "Failed to process request body", http.StatusInternalServerError)
76+
return
77+
}
78+
r.Body = io.NopCloser(bytes.NewBuffer(modifiedBody))
79+
r.ContentLength = int64(len(modifiedBody))
80+
} else {
81+
// If we can't parse the body, restore original and let handler deal with it
82+
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
83+
}
84+
}
85+
86+
next.ServeHTTP(w, r)
87+
})
88+
}
89+
}

0 commit comments

Comments
 (0)