server auth

This commit is contained in:
2026-05-28 08:35:50 -04:00
parent 3d44a392f1
commit fc63f9c44a
37 changed files with 3877 additions and 74 deletions

View File

@@ -5,9 +5,12 @@ import (
"fmt"
"net"
"net/http"
"strings"
"time"
"github.com/go-chi/chi/v5/middleware"
"github.com/tim/cairnquire/apps/server/internal/auth"
)
func (s *Server) timeoutExceptWebSocket(timeout time.Duration) func(http.Handler) http.Handler {
@@ -49,6 +52,88 @@ func (s *Server) securityHeaders(next http.Handler) http.Handler {
})
}
func (s *Server) authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
principal, ok := s.authenticateRequest(r)
if ok {
r = r.WithContext(withPrincipal(r.Context(), principal))
}
required, protected := requiredScopeForPath(r)
if !protected {
next.ServeHTTP(w, r)
return
}
if !ok {
writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"})
return
}
if !auth.Allows(principal, required) {
writeJSONWithStatus(w, http.StatusForbidden, map[string]string{"error": "forbidden"})
return
}
next.ServeHTTP(w, r)
})
}
func (s *Server) authenticateRequest(r *http.Request) (auth.Principal, bool) {
if header := r.Header.Get("Authorization"); strings.HasPrefix(header, "Bearer ") {
principal, err := s.auth.ValidateBearerToken(r.Context(), strings.TrimSpace(strings.TrimPrefix(header, "Bearer ")))
if err == nil {
return principal, true
}
s.logger.Warn("invalid bearer token", "error", err)
return auth.Principal{}, false
}
cookie, err := r.Cookie(auth.SessionCookieName)
if err != nil || cookie.Value == "" {
return auth.Principal{}, false
}
principal, err := s.auth.ValidateSessionToken(r.Context(), cookie.Value)
if err != nil {
return auth.Principal{}, false
}
return principal, true
}
func requiredScopeForPath(r *http.Request) (auth.Scope, bool) {
path := r.URL.Path
method := r.Method
switch {
case strings.HasPrefix(path, "/api/auth/"):
if path == "/api/auth/me" || path == "/api/auth/logout" || path == "/api/auth/password/change" || path == "/api/auth/account" {
return auth.ScopeDocsRead, true
}
return "", false
case strings.HasPrefix(path, "/api/device/"):
if path == "/api/device/approve" || path == "/api/device/verify" {
return auth.ScopeDocsRead, true
}
return "", false
case strings.HasPrefix(path, "/api/tokens"):
return auth.ScopeDocsRead, true
case path == "/ws":
return auth.ScopeDocsRead, true
case strings.HasPrefix(path, "/api/admin/"):
return auth.ScopeAdmin, true
case strings.HasPrefix(path, "/api/documents/") && method != http.MethodGet:
return auth.ScopeDocsWrite, true
case strings.HasPrefix(path, "/api/uploads"):
return auth.ScopeDocsWrite, true
case strings.HasPrefix(path, "/api/sync/"):
if method == http.MethodPost {
return auth.ScopeSyncWrite, true
}
return auth.ScopeSyncRead, true
case strings.HasPrefix(path, "/api/content/"):
return auth.ScopeSyncRead, true
case strings.HasSuffix(path, "/edit"):
return auth.ScopeDocsWrite, true
default:
return "", false
}
}
type statusWriter struct {
http.ResponseWriter
status int