From ff2234056135a01d9c13844fc074513e7e8403e7 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Tue, 21 Jul 2026 16:38:24 +0100 Subject: [PATCH] feat(auth): host-based org resolution + session/host match guard --- server/internal/auth/middleware.go | 6 +++ server/internal/auth/orghost.go | 66 ++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 server/internal/auth/orghost.go diff --git a/server/internal/auth/middleware.go b/server/internal/auth/middleware.go index dfd2ec1..6edb5e6 100644 --- a/server/internal/auth/middleware.go +++ b/server/internal/auth/middleware.go @@ -55,6 +55,12 @@ func Middleware() gin.HandlerFunc { } c.Set(ctxSessionKey, sess) + + if hostOrg, ok := OrgFromHost(c); ok && hostOrg.OrgID != sess.OrgID { + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "org host mismatch"}) + return + } + c.Next() } } diff --git a/server/internal/auth/orghost.go b/server/internal/auth/orghost.go new file mode 100644 index 0000000..2843bc3 --- /dev/null +++ b/server/internal/auth/orghost.go @@ -0,0 +1,66 @@ +package auth + +import ( + "strings" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/mrhid6/vantage/server/internal/models" + "github.com/mrhid6/vantage/server/internal/services" +) + +type cachedOrg struct { + org *models.Org + at time.Time +} + +var ( + orgCacheMu sync.Mutex + orgCache = map[string]cachedOrg{} +) + +const orgCacheTTL = 60 * time.Second + +// hostSlug extracts the leftmost DNS label if the host is a subdomain of the +// app root. Returns "" for the apex or an unknown host shape. +func hostSlug(host string) string { + host = strings.ToLower(host) + if i := strings.IndexByte(host, ':'); i >= 0 { + host = host[:i] + } + // Expect .vantage.<...>; apex is vantage.<...> + parts := strings.Split(host, ".") + if len(parts) < 3 { + return "" + } + if parts[1] != "vantage" { + return "" + } + if parts[0] == "vantage" || parts[0] == "www" { + return "" + } + return parts[0] +} + +func OrgFromHost(c *gin.Context) (*models.Org, bool) { + slug := hostSlug(c.Request.Host) + if slug == "" { + return nil, false + } + orgCacheMu.Lock() + if e, ok := orgCache[slug]; ok && time.Since(e.at) < orgCacheTTL { + orgCacheMu.Unlock() + return e.org, e.org != nil + } + orgCacheMu.Unlock() + + org, err := services.GetOrgBySlug(slug) + if err != nil { + org = nil + } + orgCacheMu.Lock() + orgCache[slug] = cachedOrg{org: org, at: time.Now()} + orgCacheMu.Unlock() + return org, org != nil +}