package middleware import ( "crypto/subtle" "net/http" "os" "strings" "sync" "github.com/gin-gonic/gin" ) // InternalAuthMiddleware protects the internal routes the edge services // (tracking, forms) call, with the static INTERNAL_API_TOKEN bearer. It fails // closed when the token is unset; the compare is constant-time. func (h *Handler) InternalAuthMiddleware() gin.HandlerFunc { return internalAuth } var ( internalTokenOnce sync.Once internalToken []byte ) func loadInternalToken() { if v := os.Getenv("INTERNAL_API_TOKEN"); v != "" { internalToken = []byte(v) } } func internalAuth(c *gin.Context) { internalTokenOnce.Do(loadInternalToken) if len(internalToken) == 0 { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "internal auth not configured"}) return } header := c.GetHeader("Authorization") if !strings.HasPrefix(header, "Bearer ") { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing bearer token"}) return } provided := []byte(strings.TrimPrefix(header, "Bearer ")) if subtle.ConstantTimeCompare(provided, internalToken) != 1 { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid bearer token"}) return } c.Next() } // NodeBrokerAuthMiddleware protects the internal routes only fleet nodes // call: key material, blob signing, the message map, sync lookups, worker // config and the node heartbeat. // // NODE_BROKER_TOKEN is that credential, kept apart from the one the // internet-facing tracking and forms services carry. It falls back to // INTERNAL_API_TOKEN when unset, so a single-token deployment keeps working. func (h *Handler) NodeBrokerAuthMiddleware() gin.HandlerFunc { return nodeBrokerAuth } var ( brokerTokenOnce sync.Once brokerToken []byte ) func loadBrokerToken() { if v := os.Getenv("NODE_BROKER_TOKEN"); v != "" { brokerToken = []byte(v) return } brokerToken = []byte(os.Getenv("INTERNAL_API_TOKEN")) } func nodeBrokerAuth(c *gin.Context) { brokerTokenOnce.Do(loadBrokerToken) checkNodeToken(c, brokerToken, nil) } // NodeAuthMiddleware guards node-only routes on NODE_BROKER_TOKEN; NODE_ACCEPT_INTERNAL_TOKEN=true also admits INTERNAL_API_TOKEN while older nodes upgrade. func (h *Handler) NodeAuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { brokerTokenOnce.Do(loadBrokerToken) var legacy []byte if os.Getenv("NODE_ACCEPT_INTERNAL_TOKEN") == "true" { legacy = []byte(os.Getenv("INTERNAL_API_TOKEN")) } checkNodeToken(c, brokerToken, legacy) } } // nodeUpdateOnlyKey marks a heartbeat that authenticated with INTERNAL_API_TOKEN where NODE_BROKER_TOKEN is required. const nodeUpdateOnlyKey = "node_update_only" // NodeHeartbeatAuthMiddleware guards the heartbeat like NodeAuthMiddleware, but // admits INTERNAL_API_TOKEN as update-only so a node on an older release can // always learn the version that sends NODE_BROKER_TOKEN. func (h *Handler) NodeHeartbeatAuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { brokerTokenOnce.Do(loadBrokerToken) shared := []byte(os.Getenv("INTERNAL_API_TOKEN")) if os.Getenv("NODE_ACCEPT_INTERNAL_TOKEN") == "true" { checkNodeToken(c, brokerToken, shared) return } if len(shared) > 0 && subtle.ConstantTimeCompare(shared, brokerToken) != 1 && bearerIs(c, shared) { c.Set(nodeUpdateOnlyKey, true) c.Next() return } checkNodeToken(c, brokerToken, nil) } } // IsNodeUpdateOnly reports whether the heartbeat may only be told its version, not recorded. func IsNodeUpdateOnly(c *gin.Context) bool { return c.GetBool(nodeUpdateOnlyKey) } func bearerIs(c *gin.Context, token []byte) bool { header := c.GetHeader("Authorization") if !strings.HasPrefix(header, "Bearer ") { return false } return subtle.ConstantTimeCompare([]byte(strings.TrimPrefix(header, "Bearer ")), token) == 1 } func checkNodeToken(c *gin.Context, token, legacy []byte) { if len(token) == 0 { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "internal auth not configured"}) return } header := c.GetHeader("Authorization") if !strings.HasPrefix(header, "Bearer ") { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing bearer token"}) return } provided := []byte(strings.TrimPrefix(header, "Bearer ")) ok := subtle.ConstantTimeCompare(provided, token) == 1 if !ok && len(legacy) > 0 { ok = subtle.ConstantTimeCompare(provided, legacy) == 1 } if !ok { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid bearer token"}) return } c.Next() }