package httpapi import ( "log" "net/http" "strings" "git.librete.ch/public/librenotes/internal/auth" ) // AuthMiddleware validates the Authorization: Bearer header on // every request. On success the verified Tenant is attached to the // request context so downstream handlers can scope their work. On any // failure (missing header, wrong scheme, invalid/expired/forged JWT) // the request is rejected with 401 — the failure reason is logged // server-side but not surfaced to the client to avoid hinting at // validation internals. func AuthMiddleware(signer *auth.Signer, logger *log.Logger) func(http.Handler) http.Handler { if logger == nil { logger = log.Default() } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tok, err := bearerToken(r.Header.Get("Authorization")) if err != nil { logger.Printf("auth: %v from %s", err, r.RemoteAddr) http.Error(w, "unauthorized", http.StatusUnauthorized) return } claims, err := signer.Verify(tok) if err != nil { logger.Printf("auth: jwt verify failed for %s: %v", r.RemoteAddr, err) http.Error(w, "unauthorized", http.StatusUnauthorized) return } ctx := WithTenant(r.Context(), Tenant{ UserID: claims.UserID, Email: claims.Email, }) next.ServeHTTP(w, r.WithContext(ctx)) }) } } func bearerToken(header string) (string, error) { const prefix = "Bearer " if header == "" { return "", errMissingHeader } if !strings.HasPrefix(header, prefix) { return "", errBadScheme } tok := strings.TrimSpace(header[len(prefix):]) if tok == "" { return "", errEmptyToken } return tok, nil } // Sentinel errors for log diagnostics. Not exported; clients always // see "unauthorized". var ( errMissingHeader = strErr("missing Authorization header") errBadScheme = strErr("expected Bearer scheme") errEmptyToken = strErr("empty bearer token") ) type strErr string func (e strErr) Error() string { return string(e) } // RequireTenantOwnership compares the tenant on the request with the // owner of the resource. Returns true if access is allowed; otherwise // writes 403 to w and returns false. // // Handlers that mutate or read tenant-owned resources should call this // before serving the response. The middleware ensures a Tenant is on // the context; the handler's job is to ensure the *resource* belongs // to that tenant. func RequireTenantOwnership(w http.ResponseWriter, r *http.Request, ownerID string) bool { t, err := TenantFrom(r.Context()) if err != nil { http.Error(w, "unauthorized", http.StatusUnauthorized) return false } if t.UserID != ownerID { http.Error(w, "forbidden", http.StatusForbidden) return false } return true }