CSRF Protection
Protect your Velocity application against cross-site request forgery attacks with built-in CSRF middleware.
Velocity provides comprehensive CSRF (Cross-Site Request Forgery) protection to secure your application against unauthorized form submissions and state-changing requests.
Quick Start
import (
"github.com/velocitykode/velocity/pkg/csrf"
"github.com/velocitykode/velocity/pkg/router"
)
func main() {
// Create CSRF protection with default configuration
csrfConfig := csrf.DefaultConfig()
csrfProtection := csrf.New(csrfConfig)
// Set global instance for template helpers
csrf.SetGlobalCSRF(csrfProtection)
// Create router
r := router.Get()
// Apply CSRF middleware globally
r.Use(csrf.Middleware())
// Your routes
r.Post("/submit-form", handleFormSubmission)
r.Post("/update-profile", handleProfileUpdate)
router.LoadRoutes()
}import (
"github.com/velocitykode/velocity/pkg/csrf"
"github.com/velocitykode/velocity/pkg/router"
)
func setupCSRF() {
// Configure CSRF with custom settings
config := csrf.DefaultConfig()
config.ExcludePaths = []string{
"/api/webhooks/*", // Webhook endpoints
"/health", // Health check
"/metrics", // Metrics endpoint
}
// Custom error handler for API responses
config.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(419)
json.NewEncoder(w).Encode(map[string]string{
"error": "CSRF token validation failed",
"code": "CSRF_INVALID",
})
}
protection := csrf.New(config)
csrf.SetGlobalCSRF(protection)
}<!-- In your HTML forms -->
<form method="POST" action="/submit">
{{ csrfField .SessionID }}
<input type="text" name="email" />
<button type="submit">Submit</button>
</form>
<!-- Or using meta tag for JavaScript -->
<head>
{{ csrfMeta .SessionID }}
</head>
<script>
// Access token in JavaScript
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>// For SPA/API applications
func setupAPICSRF() {
config := csrf.DefaultConfig()
// Use header-based token validation
config.HeaderName = "X-CSRF-Token"
// Exclude read-only API endpoints
config.ExcludeFunc = func(r *http.Request) bool {
// Skip CSRF for API keys
return r.Header.Get("Authorization") != ""
}
protection := csrf.New(config)
csrf.SetGlobalCSRF(protection)
// Provide token refresh endpoint
r := router.Get()
r.Get("/csrf/token", protection.RefreshHandler())
}Configuration
Default Configuration
config := csrf.DefaultConfig()
// Returns:
// {
// TokenLifetime: 24 * time.Hour,
// HeaderName: "X-CSRF-Token",
// FormField: "_token",
// CookieName: "csrf_token",
// SessionCookieName: "session_id",
// SameSite: http.SameSiteLaxMode,
// Secure: true,
// HTTPOnly: true,
// SingleUse: false,
// ErrorMessage: "CSRF token validation failed. Please refresh and try again.",
// }Custom Configuration
config := &csrf.Config{
// Token settings
TokenLifetime: 12 * time.Hour, // Token expiration
HeaderName: "X-CSRF-Token", // Header name for token
FormField: "_token", // Form field name
CookieName: "csrf_token", // Cookie name
SessionCookieName: "velocity_session", // Session cookie name
// Security settings
SameSite: http.SameSiteStrictMode, // CSRF protection level
Secure: true, // HTTPS only
HTTPOnly: true, // No JavaScript access
SingleUse: false, // Reusable tokens
// Storage
Store: csrf_stores.NewSessionStore(), // Token storage
// Exception handling
ExcludePaths: []string{ // Paths to exclude
"/api/webhooks/*",
"/health",
},
ExcludeFunc: func(r *http.Request) bool {
// Custom exclusion logic
return strings.HasPrefix(r.URL.Path, "/public/")
},
// Error handling
ErrorMessage: "Invalid CSRF token",
ErrorHandler: customErrorHandler,
}Token Storage Strategies
Session Store (Default)
Server-side token storage using sessions. Most secure for traditional web applications.
import "github.com/velocitykode/velocity/pkg/csrf/stores"
config := csrf.DefaultConfig()
config.Store = stores.NewSessionStore()Pros:
- Most secure (server-side validation)
- Works with server-side sessions
- Tokens never exposed to client
Cons:
- Requires session management
- Not suitable for stateless APIs
Custom Store Implementation
type CustomStore struct {
cache map[string]string
mu sync.RWMutex
}
func (s *CustomStore) Get(id string) (string, error) {
s.mu.RLock()
defer s.mu.RUnlock()
token, exists := s.cache[id]
if !exists {
return "", csrf.ErrTokenNotFound
}
return token, nil
}
func (s *CustomStore) Set(id string, token string) error {
s.mu.Lock()
defer s.mu.Unlock()
s.cache[id] = token
return nil
}
func (s *CustomStore) Delete(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.cache, id)
return nil
}
// Use custom store
config.Store = &CustomStore{
cache: make(map[string]string),
}Template Helpers
Velocity provides template functions for easy CSRF token inclusion:
CSRFField
Generates a hidden input field with the CSRF token:
// In your handler
func ShowForm(ctx *router.Context) error {
sessionID := getSessionID(ctx.Request)
return view.Render(ctx.Response, ctx.Request, "form", view.Props{
"SessionID": sessionID,
})
}<!-- In your template -->
<form method="POST" action="/submit">
{{ csrfField .SessionID }}
<input type="email" name="email" />
<button type="submit">Submit</button>
</form>
<!-- Renders as: -->
<!-- <input type="hidden" name="_token" value="generated-token-here"> -->CSRFMeta
Generates a meta tag for JavaScript/AJAX:
<head>
{{ csrfMeta .SessionID }}
</head>
<!-- Renders as: -->
<!-- <meta name="csrf-token" content="generated-token-here"> -->CSRFToken
Returns the raw token value:
token := csrf.CSRFToken(sessionID)
// Use token directly in codeMiddleware Integration
Global Middleware
Apply CSRF protection to all routes:
func main() {
r := router.Get()
// Apply globally
r.Use(csrf.Middleware())
// All routes protected
r.Post("/submit", handleSubmit)
r.Put("/update", handleUpdate)
}Route-Specific Middleware
Apply to specific routes or groups:
func init() {
router.Register(func(r router.Router) {
// Public routes without CSRF
r.Get("/", homeHandler.Index)
// Protected routes with CSRF
protected := r.Group("/account")
protected.Use(csrf.Middleware())
{
protected.Post("/update", accountHandler.Update)
protected.Delete("/delete", accountHandler.Delete)
}
})
}Conditional Middleware
func ConditionalCSRF(next router.HandlerFunc) router.HandlerFunc {
return func(ctx *router.Context) error {
// Skip CSRF for API requests with bearer tokens
if strings.HasPrefix(ctx.Request.Header.Get("Authorization"), "Bearer ") {
return next(ctx)
}
// Apply CSRF for session-based requests
return csrf.Middleware()(next)(ctx)
}
}Path Exclusions
Wildcard Patterns
config.ExcludePaths = []string{
"/api/webhooks/*", // All webhook endpoints
"/health", // Exact match
"/metrics", // Exact match
"/public/*", // All public endpoints
}Custom Exclusion Logic
config.ExcludeFunc = func(r *http.Request) bool {
// Exclude if API key is present
if r.Header.Get("X-API-Key") != "" {
return true
}
// Exclude if OAuth bearer token
if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
return true
}
// Exclude specific user agents (e.g., monitoring tools)
if strings.Contains(r.UserAgent(), "Monitoring") {
return true
}
return false
}AJAX and Single Page Applications
Setting Up for SPAs
// Provide token endpoint
func setupSPA() {
r := router.Get()
// Token refresh endpoint (excluded from CSRF)
config := csrf.DefaultConfig()
config.ExcludePaths = []string{"/csrf/token"}
protection := csrf.New(config)
csrf.SetGlobalCSRF(protection)
r.Get("/csrf/token", protection.RefreshHandler())
}JavaScript Integration
// Fetch token on page load
async function getCSRFToken() {
const response = await fetch('/csrf/token');
const data = await response.json();
return data.token;
}
// Use in AJAX requests
async function submitForm(formData) {
const token = await getCSRFToken();
const response = await fetch('/api/submit', {
method: 'POST',
headers: {
'X-CSRF-Token': token,
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
return response.json();
}
// Or store in meta tag and reuse
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)
});Axios Integration
// Set default header for all requests
const token = document.querySelector('meta[name="csrf-token"]').content;
axios.defaults.headers.common['X-CSRF-Token'] = token;
// Now all POST/PUT/DELETE requests include the token
axios.post('/api/submit', data);Error Handling
Default Error Response
HTML Requests:
HTTP/1.1 419 Authentication Timeout
Content-Type: text/plain
CSRF token validation failed. Please refresh and try again.JSON Requests:
{
"error": "CSRF token invalid",
"code": 419,
"message": "CSRF token validation failed. Please refresh and try again."
}Custom Error Handler
config.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
// Log the error
log.Warn("CSRF validation failed",
"error", err,
"ip", r.RemoteAddr,
"path", r.URL.Path,
)
// Check if it's a JSON request
if strings.Contains(r.Header.Get("Accept"), "application/json") {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(419)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "csrf_validation_failed",
"message": "Your session has expired. Please refresh the page.",
})
return
}
// Render custom error page for HTML requests
w.WriteHeader(419)
template.Must(template.ParseFiles("views/errors/csrf.html")).Execute(w, nil)
}Security Considerations
HTTP Status Code 419
Velocity uses status code 419 Authentication Timeout for CSRF failures, following Laravel’s convention. This distinguishes CSRF errors from other validation errors.
SameSite Cookie Attribute
// Strict: Maximum CSRF protection, may break cross-site navigation
config.SameSite = http.SameSiteStrictMode
// Lax: Balanced protection (default, recommended)
config.SameSite = http.SameSiteLaxMode
// None: Minimal protection, requires Secure=true
config.SameSite = http.SameSiteNoneModeSingle-Use Tokens
// Enable single-use tokens for maximum security
config.SingleUse = true
// Note: Requires token refresh after each request
// Best for high-security operationsToken Lifetime
// Short lifetime for high-security applications
config.TokenLifetime = 1 * time.Hour
// Longer lifetime for better UX
config.TokenLifetime = 24 * time.HourBest Practices
- Always Use CSRF for State-Changing Operations: Protect POST, PUT, DELETE, PATCH requests
- Exclude Read-Only Operations: GET, HEAD, OPTIONS don’t need CSRF protection
- Use HTTPS in Production: Set
Secure: trueto prevent token interception - Implement Token Refresh: Provide
/csrf/tokenendpoint for SPAs - Set Appropriate SameSite: Use
LaxorStrictbased on your needs - Monitor CSRF Failures: Log failures to detect potential attacks
- Handle Expired Tokens Gracefully: Show user-friendly error messages
Testing
func TestCSRFProtection(t *testing.T) {
// Create CSRF protection
config := csrf.DefaultConfig()
protection := csrf.New(config)
csrf.SetGlobalCSRF(protection)
// Create test handler
handler := csrf.Middleware()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
// Test POST without token (should fail)
req := httptest.NewRequest("POST", "/submit", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, 419, rec.Code)
// Test POST with valid token (should succeed)
sessionID := "test-session"
token, _ := protection.GetToken(sessionID)
req = httptest.NewRequest("POST", "/submit", nil)
req.Header.Set("X-CSRF-Token", token)
req.AddCookie(&http.Cookie{
Name: "session_id",
Value: sessionID,
})
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
// Test GET request (should always pass)
req = httptest.NewRequest("GET", "/page", nil)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
}Troubleshooting
Token Validation Always Failing
Problem: CSRF validation fails even with valid tokens
Solutions:
- Verify session cookie is being sent
- Check
SessionCookieNamematches your session cookie - Ensure cookies are not blocked by browser
- Verify HTTPS if
Secure: trueis set
Tokens Not Generated in Templates
Problem: csrfField returns empty string
Solutions:
- Call
csrf.SetGlobalCSRF()during initialization - Verify sessionID is passed to template
- Check CSRF middleware is registered
AJAX Requests Failing
Problem: AJAX POST/PUT/DELETE returns 419
Solutions:
- Include token in
X-CSRF-Tokenheader - Ensure token is fetched from meta tag or API
- Check token hasn’t expired
- Verify content-type header is set correctly
