mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-20 02:00:14 +00:00
feat(auth): add PAT auth, scoped REST/MCP access, CSRF, and tenant isolation
Introduce Personal Access Tokens (nzp_*) as a stateless auth path alongside
JWT, gated per-endpoint by a scope middleware (nezha:{resource}:{verb}) with
fail-closed empty-scope defaults and a server-id whitelist. Self-management
endpoints (profile, api-tokens, oauth2 bind, refresh-token) explicitly reject
PATs to block privilege-escalation chains. A revoke registry tears down active
long-lived connections (terminal, fm, ws, transfer, mcp) the moment a PAT is
deleted, with a tombstone closing the revoke->register race.
Add an MCP endpoint that proxies tool calls (exec, fs read/write/delete,
transfer) to agents over gRPC, guarded by origin/DNS-rebinding checks, a
per-token rate limiter, audit logging, and a kill switch. Serialize all
sends through the IOStream wrapper to honour grpc-go's concurrency contract.
Add CSRF double-submit protection on unsafe cookie-authenticated methods,
exempting authenticated PAT requests by context identity (not a forgeable
Authorization header). Apply visibility/whitelist filtering consistently
across list, get-by-id, and mutate paths to enforce tenant isolation.
Migrate legacy mcp:* scopes: rewrite read/exec to nezha:* equivalents and
drop dangerous write/delete/wildcard grants.
Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/nezhahq/nezha/model"
|
||||
)
|
||||
|
||||
// setCSRFCookie issues a fresh CSRF token cookie. Called by login + refresh
|
||||
// handlers so the frontend always has a paired value to mirror back into
|
||||
// the X-CSRF-Token header. The cookie is intentionally HttpOnly=false —
|
||||
// SPA JS must be able to read it. SameSite=Strict here (not Lax) because
|
||||
// the cookie's sole purpose is the same-origin double-submit check and we
|
||||
// don't want it leaking on cross-site GET navigation either.
|
||||
func setCSRFCookie(c *gin.Context) {
|
||||
var b [32]byte
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
return
|
||||
}
|
||||
c.SetSameSite(http.SameSiteStrictMode)
|
||||
c.SetCookie(csrfCookieName, hex.EncodeToString(b[:]), 0, "/", "", false, false)
|
||||
}
|
||||
|
||||
const (
|
||||
csrfCookieName = "nz-csrf"
|
||||
csrfHeaderName = "X-CSRF-Token"
|
||||
)
|
||||
|
||||
// csrfMiddleware enforces a double-submit-cookie CSRF gate on unsafe
|
||||
// HTTP methods for cookie-authenticated requests.
|
||||
//
|
||||
// Why: SameSite=Lax on the nz-jwt cookie blocks the simplest cross-site
|
||||
// form POST, but it does not stop same-site XSS-pivot CSRF, header method
|
||||
// override, redirect-leaking auth helpers, or carefully chained sub-domain
|
||||
// attacks. The double-submit pattern (server sets a JS-readable nz-csrf
|
||||
// cookie, client mirrors the value into X-CSRF-Token) closes the gap
|
||||
// without coupling auth state to a server-side session.
|
||||
//
|
||||
// Bypass conditions:
|
||||
// - Safe methods (GET/HEAD/OPTIONS): no state mutation, no CSRF risk.
|
||||
// - Bearer-token PAT requests (`Authorization: Bearer nzp_*`): stateless,
|
||||
// no ambient cookie, so a CSRF attack cannot induce them.
|
||||
//
|
||||
// Reject conditions:
|
||||
// - Missing or empty X-CSRF-Token header.
|
||||
// - Missing or empty nz-csrf cookie.
|
||||
// - Header value != cookie value.
|
||||
//
|
||||
// The middleware DOES NOT set the csrf cookie on its own — that is the
|
||||
// JWT login / refresh handler's job, since those are the only places that
|
||||
// know when to mint a fresh value. The pair just has to exist by the time
|
||||
// any unsafe call reaches here.
|
||||
func csrfMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
switch c.Request.Method {
|
||||
case http.MethodGet, http.MethodHead, http.MethodOptions:
|
||||
// Self-heal sessions that predate the CSRF cookie (or whose
|
||||
// nz-csrf expired): seed a fresh value on a safe method so the
|
||||
// double-submit pair exists before the next unsafe call. Safe
|
||||
// methods mutate nothing, so minting here carries no CSRF risk.
|
||||
if cookie, err := c.Cookie(csrfCookieName); err != nil || cookie == "" {
|
||||
setCSRFCookie(c)
|
||||
}
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
// A PAT request carries no ambient cookie, so CSRF cannot induce it.
|
||||
// The exemption must check the authenticated PAT identity resolved by
|
||||
// apiTokenAuthMiddleware, not a forgeable Authorization header value.
|
||||
if APITokenFromContext(c) != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
header := c.GetHeader(csrfHeaderName)
|
||||
cookie, err := c.Cookie(csrfCookieName)
|
||||
if err != nil || cookie == "" || header == "" || header != cookie {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, model.CommonResponse[any]{
|
||||
Success: false,
|
||||
Error: "ApiErrorForbidden: missing or invalid CSRF token",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user