package middleware import ( "crypto/subtle" "net/http" "strings" "github.com/gin-gonic/gin" ) // AdminAuth validates Authorization: Bearer against a list of // allowed admin tokens. On failure it aborts with 401 and a JSON error body. // Successful requests have the matched token stored under "admin_token". func AdminAuth(adminTokens []string) gin.HandlerFunc { return func(c *gin.Context) { token := bearerToken(c) if token == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing bearer token"}) return } if !matchAny(token, adminTokens) { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid bearer token"}) return } c.Set("admin_token", token) c.Next() } } // AgentAuth validates a single Authorization: Bearer against the // configured agent token. It is intended for the LLM-facing /mcp endpoint. func AgentAuth(agentToken string) gin.HandlerFunc { return func(c *gin.Context) { token := bearerToken(c) if token == "" || subtle.ConstantTimeCompare([]byte(token), []byte(agentToken)) != 1 { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } c.Set("agent_token", token) c.Next() } } func bearerToken(c *gin.Context) string { h := c.GetHeader("Authorization") if h == "" { return "" } const prefix = "Bearer " if !strings.HasPrefix(h, prefix) { return "" } return strings.TrimSpace(h[len(prefix):]) } // matchAny compares the provided token against every candidate using a // constant-time comparison to reduce timing side-channels. func matchAny(token string, candidates []string) bool { t := []byte(token) for _, c := range candidates { if subtle.ConstantTimeCompare(t, []byte(c)) == 1 { return true } } return false }