fix(security): API keys only deliver data; one auth check everywhere

Until API keys get scopes they have exactly one right: POST /api/v1/inbox
(thought or task via target) and the webhook ingest; every other endpoint
answers 403 for a key. Middleware and webhook ingest share one token check
(revocation list, token_version, active account), closing FLAW-08.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-29 07:39:29 +01:00
1 parent 62463bc6fd
commit 300fd6a7be
4 files changed
+99 -49

No files matched your search

+51 -46
View File
@@ -368,44 +368,58 @@ func (s *Server) apiLoggingMiddleware(next http.Handler) http.Handler {
})
}
// authenticateToken resolves a session JWT or an API key (mindsift_sec_…) to an active user. JWTs are
// checked against the revocation list and token_version; viaAPIKey tells the caller which it was.
func (s *Server) authenticateToken(token string) (user *domain.User, viaAPIKey bool) {
token = strings.TrimSpace(token)
if token == "" {
return nil, false
}
if strings.HasPrefix(token, "mindsift_sec_") {
u, err := s.store.ValidateAPIKeyAndGetUser(token)
if err != nil || u == nil || !u.IsActive {
return nil, true
}
return u, true
}
claims, err := auth.ValidateToken(s.cfg.JWTSecret, token)
if err != nil || claims == nil || s.store.IsTokenRevoked(token) {
return nil, false
}
u, err := s.store.GetUserByID(claims.UserID)
if err != nil || u == nil || !u.IsActive {
return nil, false
}
if claims.TokenVersion != 0 && claims.TokenVersion != u.TokenVersion {
return nil, false
}
return u, false
}
// apiKeyAllowed: until API keys get scopes, a key has exactly one right – delivering data into MindSift
// (POST /api/v1/inbox; /api/v1/webhooks/ingest authenticates on its own). Everything else needs a login.
func apiKeyAllowed(r *http.Request) bool {
return r.Method == http.MethodPost && r.URL.Path == "/api/v1/inbox"
}
func (s *Server) authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 1. Check Authorization Bearer Token
authHeader := r.Header.Get("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
if strings.HasPrefix(tokenStr, "mindsift_sec_") {
user, err := s.store.ValidateAPIKeyAndGetUser(tokenStr)
if err == nil && user != nil && user.IsActive {
next.ServeHTTP(w, r.WithContext(auth.WithUserID(r.Context(), user.ID)))
return
}
}
claims, err := auth.ValidateToken(s.cfg.JWTSecret, tokenStr)
if err == nil && claims != nil {
if !s.store.IsTokenRevoked(tokenStr) {
user, userErr := s.store.GetUserByID(claims.UserID)
if userErr == nil && user != nil && user.IsActive {
if claims.TokenVersion == 0 || claims.TokenVersion == user.TokenVersion {
next.ServeHTTP(w, r.WithContext(auth.WithUserID(r.Context(), user.ID)))
return
}
}
}
}
token := ""
if authHeader := r.Header.Get("Authorization"); strings.HasPrefix(authHeader, "Bearer ") {
token = strings.TrimPrefix(authHeader, "Bearer ")
} else {
token = r.Header.Get("X-API-Key")
}
// 2. Check X-API-Key Header
apiKey := r.Header.Get("X-API-Key")
if apiKey != "" {
user, err := s.store.ValidateAPIKeyAndGetUser(apiKey)
if err == nil && user != nil && user.IsActive {
next.ServeHTTP(w, r.WithContext(auth.WithUserID(r.Context(), user.ID)))
return
}
user, viaAPIKey := s.authenticateToken(token)
if user == nil {
writeError(w, http.StatusUnauthorized, "Nicht autorisiert – bitte anmelden")
return
}
writeError(w, http.StatusUnauthorized, "Nicht autorisiert – bitte anmelden")
if viaAPIKey && !apiKeyAllowed(r) {
writeError(w, http.StatusForbidden, "API-Keys dürfen nur Daten einliefern (POST /api/v1/inbox oder /api/v1/webhooks/ingest)")
return
}
next.ServeHTTP(w, r.WithContext(auth.WithUserID(r.Context(), user.ID)))
})
}
@@ -1983,18 +1997,9 @@ func (s *Server) handleWebhookIngest(w http.ResponseWriter, r *http.Request) {
token = strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
}
var user *domain.User
var err error
if strings.HasPrefix(token, "mindsift_sec_") {
user, err = s.store.ValidateAPIKeyAndGetUser(token)
} else if token != "" {
if claims, jwtErr := auth.ValidateToken(s.cfg.JWTSecret, token); jwtErr == nil {
user, err = s.store.GetUserByID(claims.UserID)
}
}
if err != nil || user == nil {
// Same checks as every other request (revoked/outdated sessions, inactive accounts)
user, _ := s.authenticateToken(token)
if user == nil {
writeError(w, http.StatusUnauthorized, "Ungültiger Webhook-Token oder API-Key")
return
}