Skip to main content

Overview

The csrf middleware protects against Cross-Site Request Forgery attacks by generating and validating tokens. It ensures that form submissions originate from your application, not from malicious sites. Use it when you have:
  • HTML forms that modify data
  • Server-rendered applications
  • Any POST/PUT/DELETE endpoints accessed via browser

Installation

import "github.com/go-mizu/mizu/middlewares/csrf"

Quick Start

app := mizu.New()

// Production: secure cookies
app.Use(csrf.Protect([]byte("32-byte-secret-key-here-12345")))

// Development: insecure cookies (HTTP)
app.Use(csrf.ProtectDev([]byte("32-byte-secret-key-here-12345")))

Configuration

Options

OptionTypeDefaultDescription
Secret[]byterequiredSecret key for token generation
TokenLengthint32Length of random token
TokenLookupstring"header:X-CSRF-Token"Where to find token in request
CookieNamestring"_csrf"Name of CSRF cookie
CookiePathstring"/"Cookie path
CookieMaxAgeint86400Cookie max age in seconds
CookieSecureboolfalseSecure cookie flag
CookieHTTPOnlybooltrueHTTPOnly cookie flag
SameSitehttp.SameSiteLaxSameSite cookie attribute
ErrorHandlerfunc(*mizu.Ctx, error) error-Custom error handler
SkipPaths[]string-Paths to skip CSRF validation

TokenLookup Format

The TokenLookup option uses "source:name" format:
SourceExampleDescription
header"header:X-CSRF-Token"Read from request header
form"form:_csrf"Read from form field
query"query:csrf"Read from query parameter

Examples

Basic Form Protection

secret := csrf.GenerateSecret() // Or use a fixed 32-byte secret

app.Use(csrf.New(csrf.Options{
    Secret:       secret,
    CookieSecure: true, // Use in production with HTTPS
}))

app.Get("/form", func(c *mizu.Ctx) error {
    token := csrf.Token(c)
    return c.HTML(200, `
        <form method="POST" action="/submit">
            <input type="hidden" name="_csrf" value="`+token+`">
            <input type="text" name="data">
            <button type="submit">Submit</button>
        </form>
    `)
})

app.Post("/submit", func(c *mizu.Ctx) error {
    // CSRF validated automatically
    return c.Text(200, "Form submitted!")
})

Using Template Field Helper

app.Get("/form", func(c *mizu.Ctx) error {
    // TemplateField returns a complete hidden input element
    field := csrf.TemplateField(c)
    return c.HTML(200, `
        <form method="POST" action="/submit">
            `+field+`
            <input type="text" name="data">
            <button type="submit">Submit</button>
        </form>
    `)
})

JavaScript AJAX Requests

app.Use(csrf.New(csrf.Options{
    Secret:      secret,
    TokenLookup: "header:X-CSRF-Token",
}))
<!-- Get token from cookie or meta tag -->
<meta name="csrf-token" content="{{ .CSRFToken }}">

<script>
const token = document.querySelector('meta[name="csrf-token"]').content;

fetch('/api/data', {
    method: 'POST',
    headers: {
        'X-CSRF-Token': token,
        'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
});
</script>

Skip API Routes

app.Use(csrf.New(csrf.Options{
    Secret: secret,
    SkipPaths: []string{
        "/api/webhook",    // External webhooks
        "/api/public",     // Public API endpoints
    },
}))

Custom Error Handler

app.Use(csrf.New(csrf.Options{
    Secret: secret,
    ErrorHandler: func(c *mizu.Ctx, err error) error {
        if err == csrf.ErrTokenMissing {
            return c.HTML(403, `
                <h1>Security Error</h1>
                <p>Missing security token. Please try again.</p>
            `)
        }
        return c.HTML(403, `
            <h1>Security Error</h1>
            <p>Invalid security token. Please refresh and try again.</p>
        `)
    },
}))

Multiple Token Sources

// Accept token from either header or form
app.Use(csrf.New(csrf.Options{
    Secret:      secret,
    TokenLookup: "form:_csrf", // Primary: form field
}))

// Alternative: Check header if form field missing
// (Requires custom implementation)

Development vs Production

func setupCSRF(app *mizu.App, isDev bool) {
    secret := []byte(os.Getenv("CSRF_SECRET"))

    if isDev {
        app.Use(csrf.ProtectDev(secret))
    } else {
        app.Use(csrf.Protect(secret))
    }
}

API Reference

Functions

// New creates CSRF middleware with options
func New(opts Options) mizu.Middleware

// Protect creates middleware with secure cookies (production)
func Protect(secret []byte) mizu.Middleware

// ProtectDev creates middleware with insecure cookies (development)
func ProtectDev(secret []byte) mizu.Middleware

// GenerateSecret generates a secure random secret
func GenerateSecret() []byte

Token Functions

// Token extracts CSRF token from context
func Token(c *mizu.Ctx) string

// TemplateField returns HTML hidden input field
func TemplateField(c *mizu.Ctx) string

// TokenExpiry returns token expiration time
func TokenExpiry(opts Options) time.Time

Error Types

var (
    ErrTokenMissing // CSRF token not found in request
    ErrTokenInvalid // CSRF token validation failed
)

How It Works

  1. Token Generation: On GET requests, a token is generated and stored in a cookie
  2. Token Validation: On POST/PUT/DELETE requests, the token must be included
  3. Double Submit: The cookie token must match the request token
  4. HMAC Signature: Tokens are signed to prevent tampering
GET /form
  ← Sets _csrf cookie with token
  ← Provides token for form

POST /submit
  → Sends _csrf cookie (automatic)
  → Sends token in form/header
  ← Server validates both match

Technical Details

Token Generation

The middleware generates cryptographically secure tokens using the following process:
  1. Random Bytes: Generates random bytes of specified length (default 32) using crypto/rand
  2. HMAC Signature: Creates an HMAC-SHA256 signature of the random bytes using the secret key
  3. Encoding: Encodes both the token and signature using base64 URL encoding
  4. Format: Combines them as token.signature for tamper detection
// Token format: base64(random_bytes).base64(hmac_sha256(random_bytes))
token := generateToken(32, secret)
// Example: "a1b2c3d4...xyz.9f8e7d6c..."

Token Validation

Validation follows a multi-step process to ensure security:
  1. Constant-Time Comparison: Uses subtle.ConstantTimeCompare to prevent timing attacks
  2. Cookie-Request Match: Verifies the cookie token exactly matches the request token
  3. Signature Verification: Decodes and validates the HMAC signature
  4. HMAC Equality: Uses hmac.Equal for secure signature comparison

Safe vs Unsafe Methods

The middleware distinguishes between HTTP methods:
  • Safe Methods (GET, HEAD, OPTIONS, TRACE): Generate or reuse tokens, no validation required
  • Unsafe Methods (POST, PUT, DELETE, PATCH): Require valid token in both cookie and request

Context Storage

Tokens are stored in the request context using a private contextKey type to prevent collisions with other middleware or application code. This allows retrieval via the Token() function throughout the request lifecycle.

Path Skipping

The SkipPaths option uses a map for O(1) lookup performance, allowing specific routes (like webhooks or public APIs) to bypass CSRF validation entirely.

Security Considerations

  1. Secret Key - Use a strong, random 32-byte secret
  2. Cookie Settings - Use Secure, HttpOnly, and SameSite in production
  3. Token Rotation - Tokens are per-session, not per-request
  4. HTTPS Required - Always use HTTPS in production
app.Use(csrf.New(csrf.Options{
    Secret:         []byte(os.Getenv("CSRF_SECRET")),
    CookieSecure:   true,
    CookieHTTPOnly: true,
    SameSite:       http.SameSiteStrictMode,
    CookieMaxAge:   3600, // 1 hour
}))

Best Practices

  • Always include CSRF protection for forms
  • Use secure cookies in production
  • Skip CSRF for API endpoints using Bearer auth
  • Regenerate token after login/logout
  • Set appropriate cookie expiration

Testing

The CSRF middleware includes comprehensive test coverage for all functionality:
Test CaseDescriptionExpected Behavior
Basic Functionality
Sets cookie on GETGET request to protected endpointCSRF cookie is set with generated token
Rejects POST without tokenPOST request without CSRF tokenReturns 403 Forbidden
Accepts POST with valid tokenPOST with matching cookie and header tokenRequest succeeds with 200 OK
Rejects POST with invalid tokenPOST with mismatched tokensReturns 403 Forbidden
Token Lookup Sources
Form token lookupPOST with token in form fieldToken extracted from form and validated
Query token lookupPOST with token in query parameterToken extracted from query and validated
Header token lookupPOST with token in header (default)Token extracted from header and validated
Path Skipping
Skips listed pathPOST to path in SkipPathsRequest bypasses CSRF validation
Protects non-listed pathPOST to path not in SkipPathsCSRF validation required
Error Handling
Custom error handlerPOST without token with ErrorHandler setCustom error handler called with error
Default error handlerPOST without token, no ErrorHandlerReturns 403 with error text
Helper Functions
TemplateField generationCalling TemplateField() with contextReturns HTML hidden input with token
Protect middlewareCreating middleware with Protect()Secure cookies enabled by default
ProtectDev middlewareCreating middleware with ProtectDev()Insecure cookies for development
GenerateSecret uniquenessGenerating multiple secretsEach secret is unique and 32 bytes
Validation Logic
Valid token validationValidating matching tokens with correct secretReturns true
Mismatched tokensValidating different tokensReturns false
Invalid signatureValidating token with wrong secretReturns false
Malformed tokenValidating token without signature separatorReturns false
Token Generation
Token formatGenerated token structureContains random bytes and HMAC signature separated by ”.”
Token uniquenessGenerating 100 tokensAll tokens are unique
Configuration Validation
Panics without secretCreating middleware with empty secretPanics with error message
Panics with invalid TokenLookupCreating middleware with malformed TokenLookupPanics with error message