package server import ( "context" "net/http" "net/url" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // headerUserID is the identity header the gateway injects after resolving a // session to an internal account. const headerUserID = "X-User-ID" // contextKey is an unexported type for request-context keys set by this package. type contextKey string const userIDContextKey contextKey = "scrabble.user_id" // RequireUserID returns middleware that requires a valid X-User-ID header and // stores the parsed account id in the request context. Requests without a // parseable UUID are rejected with 401. The backend treats X-User-ID as the // sole identity input and never derives identity from the request body. func RequireUserID() gin.HandlerFunc { return func(c *gin.Context) { id, err := uuid.Parse(c.GetHeader(headerUserID)) if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing or invalid X-User-ID"}) return } c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), userIDContextKey, id)) c.Next() } } // UserIDFromContext returns the authenticated account id stored by // RequireUserID, and whether it was present. func UserIDFromContext(ctx context.Context) (uuid.UUID, bool) { id, ok := ctx.Value(userIDContextKey).(uuid.UUID) return id, ok } // requireSameOrigin guards the admin console's state-changing requests: it rejects // a non-safe request whose Origin (or, failing that, Referer) host does not match // the request Host. The gateway authenticates the operator with Basic-Auth in front // of /_gm; this same-origin check is the console's CSRF defence, stopping a // cross-site form POST from riding the cached credential. Safe methods pass through. func requireSameOrigin() gin.HandlerFunc { return func(c *gin.Context) { switch c.Request.Method { case http.MethodGet, http.MethodHead, http.MethodOptions: c.Next() return } if !sameOrigin(c.Request) { c.AbortWithStatus(http.StatusForbidden) return } c.Next() } } // sameOrigin reports whether the request's Origin (or, failing that, Referer) host // matches the request Host. A state-changing request carrying neither header is // rejected. func sameOrigin(r *http.Request) bool { for _, h := range []string{r.Header.Get("Origin"), r.Header.Get("Referer")} { if h == "" { continue } u, err := url.Parse(h) if err != nil { return false } return u.Host == r.Host } return false }