package adminconsole import ( "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/base64" "fmt" ) // CSRF issues and verifies the stateless anti-CSRF token used by the admin // console. The token is an HMAC-SHA256 over the authenticated operator's // username keyed by a process secret, so a cross-site request cannot forge it // without already being able to read an authenticated page. The console is // sessionless (HTTP Basic Auth), which makes a stateless, per-operator token // the natural fit. type CSRF struct { key []byte } // NewCSRF returns a CSRF signer keyed by key. A shared key across backend // replicas lets a form rendered by one replica validate on another; callers // that pass a per-process random key (see NewRandomCSRF) accept that forms do // not survive a restart or span replicas. func NewCSRF(key []byte) *CSRF { return &CSRF{key: key} } // NewRandomCSRF returns a CSRF signer keyed by a fresh 32-byte random secret. // It is the secure default when no shared key is configured. func NewRandomCSRF() (*CSRF, error) { key := make([]byte, 32) if _, err := rand.Read(key); err != nil { return nil, fmt.Errorf("generate admin console CSRF key: %w", err) } return &CSRF{key: key}, nil } // Token returns the anti-CSRF token bound to username. func (c *CSRF) Token(username string) string { mac := hmac.New(sha256.New, c.key) mac.Write([]byte(username)) return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) } // Verify reports whether token is the valid anti-CSRF token for username. The // comparison runs in constant time relative to the token bytes. func (c *CSRF) Verify(username, token string) bool { if token == "" { return false } expected := c.Token(username) return hmac.Equal([]byte(token), []byte(expected)) }