server auth
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user