package httpx import ( "context" "crypto/hmac" "crypto/sha256" "encoding/base64" "errors" "net/http" ) // ErrNoCSRFKey is returned when the CSRF key is empty — fail closed. var ErrNoCSRFKey = errors.New("httpx: CSRF key is required") // CSRF derives and verifies the per-session CSRF token on every action request // (SPEC-003 §6, A8). The token is HMAC-SHA256(session_id) under the config // `csrf_key`, so it is bound to the session, needs no extra table, and dies // with the session. // // The action routing fields (`do`/`action`) are NOT a security control on their // own (D-017); this token is. The session cookie is HttpOnly, so a page embeds // the token in a hidden form field via Token / the request context. type CSRF struct { key []byte } // NewCSRF builds a CSRF helper from the configured key. An empty key fails // closed. func NewCSRF(key []byte) (*CSRF, error) { if len(key) == 0 { return nil, ErrNoCSRFKey } k := make([]byte, len(key)) copy(k, key) return &CSRF{key: k}, nil } // Token returns the CSRF token bound to sessionID. func (c *CSRF) Token(sessionID string) string { mac := hmac.New(sha256.New, c.key) mac.Write([]byte("myday.csrf.v1:")) mac.Write([]byte(sessionID)) return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) } // Valid reports whether token is the correct CSRF token for sessionID. It // rejects empty input and compares in constant time. func (c *CSRF) Valid(sessionID, token string) bool { if sessionID == "" || token == "" { return false } want := c.Token(sessionID) return hmac.Equal([]byte(want), []byte(token)) } // tokenCtxKey is the unexported context key for the request's CSRF token. type tokenCtxKey struct{} // WithToken exposes the request's CSRF token on the context so a handler can // embed it in a form without re-deriving it. func WithToken(ctx context.Context, token string) context.Context { return context.WithValue(ctx, tokenCtxKey{}, token) } // TokenFrom returns the CSRF token attached by the door, if any. func TokenFrom(ctx context.Context) string { t, _ := ctx.Value(tokenCtxKey{}).(string) return t } // TokenFromRequest is a convenience for handlers: the token the door attached // to the request, ready for a hidden form field. func TokenFromRequest(r *http.Request) string { return TokenFrom(r.Context()) }