Skip to content

Defensive Security: CSRF, CSP, Rate Limiting & CORS

Building a production email server requires more than just functionality—it demands defensive security at every layer. In the email-server project, I implemented comprehensive security measures across multiple commits to protect against common vulnerabilities.

This article covers the defensive security implementations, including CSRF protection, Content Security Policy hardening, rate limiting strategies, and CORS blocking—all essential for securing web applications in production.

CSRF Protection: Defending Against Cross-Site Request Forgery

Cross-Site Request Forgery (CSRF) attacks trick authenticated users into executing unwanted actions on a web application. The email-server project encountered and resolved several CSRF-related issues through iterative improvements.

The Problem: Wizard CSRF Failures

The initial implementation followed a common pattern: generate a token, validate it on POST requests, and immediately invalidate it. However, this caused issues with multi-step wizards that required multiple AJAX requests.

Commit b175bc5 revealed the issue when the DNS wizard for domain configuration began failing. The wizard required multiple AJAX calls:

// @filename: index.js
// validateDomain() - First AJAX call
fetch('/admin/domains/wizard/validate', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({domain: domain})
})

// fetchDNSRecords() - Second AJAX call
fetch('/admin/domains/wizard/dns-records', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({...})
})

After the first call succeeded, the CSRF token was invalidated, causing subsequent calls to fail with “Invalid or expired CSRF token” errors.

Initial Approach: Single-Step Form (Commit f5156ca)

The first attempt simplified the domain form to a single step, removing the need for multiple AJAX requests:

<!-- Simple form POST instead of AJAX -->
<form method="POST" action="/admin/domains/add">
  <input type="hidden" name="csrf_token" value="{{.CSRFToken}}" />
  <div class="form-group">
    <label for="name">Domain Name</label>
    <input
      type="text"
      id="name"
      name="name"
      class="form-control"
      required
      placeholder="example.com"
      value="{{.Domain}}"
    />
  </div>
</form>

While this solved the CSRF issue, it sacrificed user experience by eliminating the helpful multi-step wizard that guided users through DNS configuration.

Final Solution: Token Reuse Until Expiration (Commit a8755f4)

The optimal approach allows CSRF tokens to be reused until they naturally expire (1 hour). This supports multi-step wizards and AJAX calls while maintaining security:

// @filename: main.go
// withCSRF wraps a handler with CSRF protection
func (s *Server) withCSRF(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Skip CSRF for GET/HEAD/OPTIONS
        if r.Method == http.MethodGet || r.Method == http.MethodHead || r.Method == http.MethodOptions {
            token := generateToken()
            csrfTokensMu.Lock()
            csrfTokens[token] = time.Now().Add(1 * time.Hour)
            csrfTokensMu.Unlock()
            w.Header().Set("X-CSRF-Token", token)
            next.ServeHTTP(w, r)
            return
        }

        // Validate CSRF token for state-changing requests
        token := r.FormValue("csrf_token")
        if token == "" {
            token = r.Header.Get("X-CSRF-Token")
        }

        if !isValidToken(token) {
            http.Error(w, "Invalid or expired CSRF token", http.StatusForbidden)
            return
        }

        csrfTokensMu.RLock()
        expiry, exists := csrfTokens[token]
        csrfTokensMu.RUnlock()

        now := time.Now()
        if !exists || now.After(expiry) {
            http.Error(w, "Invalid or expired CSRF token", http.StatusForbidden)
            return
        }

        // Keep token valid for reuse (for multi-step wizards and AJAX calls)
        // Token will naturally expire after 1 hour
        w.Header().Set("X-CSRF-Token", token)

        next.ServeHTTP(w, r)
    })
}

AJAX Request Token Handling

The wizard JavaScript now includes the CSRF token in all AJAX requests:

// @filename: index.js
var wizardData = {
    domain: '',
    selector: 'mail',
    bits: 2048,
    storage: 'database',
    stateId: '',
    records: null,
    skipped: false,
    csrfToken: '{{.CSRFToken}}'  // Embedded from server
};

function validateDomain() {
    fetch('/admin/domains/wizard/validate', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-Token': wizardData.csrfToken  // Include in header
        },
        body: JSON.stringify({domain: domain})
    })
    .then(function(r) { return r.json(); })
    .then(function(data) {
        // Handle response
    });
}

function fetchDNSRecords() {
    fetch('/admin/domains/wizard/dns-records', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-Token': wizardData.csrfToken  // Same token reused
        },
        body: JSON.stringify({...})
    })
}

Token Generation: Cryptographic Security

CSRF tokens must be cryptographically secure to prevent prediction attacks:

// @filename: handlers.go
func generateToken() string {
    b := make([]byte, 32)
    if _, err := rand.Read(b); err != nil {
        panic("crypto/rand failed: " + err.Error())
    }
    return hex.EncodeToString(b)
}

func isValidToken(token string) bool {
    // Minimum length check (32 hex chars = 16 bytes)
    if len(token) < 32 {
        return false
    }
    // Maximum length check to prevent DoS
    if len(token) > 128 {
        return false
    }
    // Validate hex encoding
    _, err := hex.DecodeString(token)
    return err == nil
}

Key Security Points:

  • Uses crypto/rand (not math/rand) for cryptographically secure randomness
  • Panics on RNG failure rather than falling back to weak tokens
  • Validates token format and length to prevent DoS attacks
  • Tokens expire after 1 hour automatically

Periodic Cleanup

CSRF tokens are cleaned up periodically to prevent memory leaks:

// @filename: handlers.go
func CleanupExpiredSessions(db *sql.DB) {
    ticker := time.NewTicker(15 * time.Minute)
    go func() {
        for range ticker.C {
            now := time.Now()

            // Clean CSRF tokens
            csrfTokensMu.Lock()
            for token, expiry := range csrfTokens {
                if now.After(expiry) {
                    delete(csrfTokens, token)
                }
            }
            csrfTokensMu.Unlock()
        }
    }()
}

Content Security Policy (CSP) Hardening

Content Security Policy is a powerful defense against XSS attacks, injection of malicious scripts, and other code injection vulnerabilities. Commit 47399d7 significantly hardened the CSP implementation.

CSP Implementation

// @filename: main.go
func (s *Server) withSecurityHeaders(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Content Security Policy - restrict resource loading
        // Note: 'unsafe-inline' needed for admin panel inline scripts
        w.Header().Set("Content-Security-Policy",
            "default-src 'self'; "+
            "script-src 'self' 'unsafe-inline'; "+ // Allow inline scripts for admin UI
            "style-src 'self' 'unsafe-inline'; "+
            "img-src 'self' data:; "+
            "font-src 'self'; "+
            "connect-src 'self'; "+
            "form-action 'self'; "+
            "frame-ancestors 'none'; "+
            "base-uri 'self'; "+
            "object-src 'none'; "+ // Block plugins
            "upgrade-insecure-requests")

        next.ServeHTTP(w, r)
    })
}

Key CSP Directives

1. object-src 'none'

This directive blocks loading of plugins (Flash, Java, PDFs, etc.) which have a history of security vulnerabilities:

object-src 'none'

Why: Plugin vulnerabilities have been exploited for years. Blocking them eliminates an entire attack surface.

2. FLoC Blocking (Interest Cohort API)

FLoC (Federated Learning of Cohorts) was Google’s controversial attempt to track users across sites without cookies. The Permissions Policy header blocks it:

// @filename: main.go
w.Header().Set("Permissions-Policy",
    "accelerometer=(), camera=(), geolocation=(), gyroscope=(), "+
    "magnetometer=(), microphone=(), payment=(), usb=(), "+
    "interest-cohort=()") // Block FLoC tracking

Why: FLoC raised privacy concerns by creating behavioral profiles without user consent. Blocking it prevents browser-based tracking.

3. Inline Script Handling

The admin panel uses inline scripts for interactivity, requiring unsafe-inline:

script-src 'self' 'unsafe-inline'

Better approach for production: Extract all inline scripts into separate files and use nonces or hashes:

script-src 'self' 'nonce-{random}' 'sha256-{hash}'

4. Frame Protection

frame-ancestors 'none'
X-Frame-Options: DENY

Prevents the admin panel from being embedded in iframes, protecting against clickjacking attacks.

5. Form Action Restriction

form-action 'self'

Prevents forms from submitting data to external domains, mitigating CSRF attempts that bypass token validation.

6. Base URI Restriction

base-uri 'self'

Prevents malicious scripts from changing the base URL, which could be used to load resources from external domains.

Other Essential Security Headers

// @filename: main.go
// Prevent clickjacking
w.Header().Set("X-Frame-Options", "DENY")

// Prevent MIME type sniffing
w.Header().Set("X-Content-Type-Options", "nosniff")

// XSS protection (legacy, but still useful for older browsers)
w.Header().Set("X-XSS-Protection", "1; mode=block")

// Referrer policy - don't leak URLs to external sites
w.Header().Set("Referrer-Policy", "no-referrer")

Rate Limiting Implementation

Rate limiting protects against brute force attacks, credential stuffing, and DoS attacks. The email-server implements multiple rate limiting strategies.

UserRateLimiter: Per-User Limits

Limits email sending per user to prevent abuse:

// @filename: handlers.go
type UserRateLimiter struct {
    mu         sync.RWMutex
    counters   map[int64]*userSendCounter
    maxPerHour int
    maxPerDay  int
    stopCleanup chan struct{}
}

type userSendCounter struct {
    hourCount  int
    dayCount   int
    hourReset  time.Time
    dayReset   time.Time
    lastAccess time.Time // Track last access for cleanup
}

func NewUserRateLimiter(maxPerHour, maxPerDay int) *UserRateLimiter {
    rl := &UserRateLimiter{
        counters:    make(map[int64]*userSendCounter),
        maxPerHour:  maxPerHour,
        maxPerDay:   maxPerDay,
        stopCleanup: make(chan struct{}),
    }
    // Start cleanup goroutine to prevent unbounded memory growth
    go rl.cleanupLoop()
    return rl
}

Memory Leak Fix: Periodic Cleanup (Commit e7b2314)

The initial implementation suffered from unbounded map growth. UserRateLimiter accumulated entries indefinitely, leading to memory leaks in long-running services.

Before:

// @filename: main.go
// No cleanup - map grows unbounded
func (rl *UserRateLimiter) CheckAndIncrement(userID int64) error {
    rl.mu.Lock()
    defer rl.mu.Unlock()

    // ... check and increment logic
    // Never removes old entries!
}

After:

// @filename: main.go
// cleanupLoop periodically removes stale entries
func (rl *UserRateLimiter) cleanupLoop() {
    ticker := time.NewTicker(15 * time.Minute)
    defer ticker.Stop()

    for {
        select {
        case <-ticker.C:
            rl.cleanup()
        case <-rl.stopCleanup:
            return
        }
    }
}

// cleanup removes entries that haven't been accessed in over 48 hours
func (rl *UserRateLimiter) cleanup() {
    rl.mu.Lock()
    defer rl.mu.Unlock()

    cutoff := time.Now().Add(-48 * time.Hour)
    for userID, counter := range rl.counters {
        if counter.lastAccess.Before(cutoff) {
            delete(rl.counters, userID)
        }
    }
}

APIRateLimiter: Per-API-Key Limits

// @filename: main.go
// RateLimiter tracks failed login attempts per IP
type RateLimiter struct {
    mu       sync.RWMutex
    attempts map[string]*attemptInfo
    // Configuration
    maxAttempts    int
    windowSize     time.Duration
    blockDuration  time.Duration
    trustedProxies map[string]bool // Only trust proxy headers from these IPs
}

type attemptInfo struct {
    count     int
    firstTime time.Time
    blockedAt time.Time
}

Trusted Proxy Configuration

For accurate rate limiting behind reverse proxies (nginx), we must trust proxy headers only from trusted sources:

// @filename: handlers.go
// DefaultRateLimiter returns a rate limiter with sensible defaults
// 5 attempts per 15 minutes, 30 minute block
// Trusts localhost as proxy (for nginx reverse proxy)
func DefaultRateLimiter() *RateLimiter {
    // Trust nginx on localhost - this allows rate limiting by actual client IP
    return NewRateLimiter(5, 15*time.Minute, 30*time.Minute, []string{"127.0.0.1", "::1"})
}

// GetClientIP extracts the client IP from the request
// Only trusts X-Forwarded-For/X-Real-IP if request comes from a trusted proxy
func (rl *RateLimiter) GetClientIP(r *http.Request) string {
    // First get the direct connection IP
    directIP, _, err := net.SplitHostPort(r.RemoteAddr)
    if err != nil {
        directIP = r.RemoteAddr
    }

    // Only trust proxy headers if the direct connection is from a trusted proxy
    if len(rl.trustedProxies) > 0 && rl.trustedProxies[directIP] {
        // Check X-Forwarded-For header (for reverse proxy)
        if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
            // Take the first IP in the chain (original client)
            ips := strings.Split(xff, ",")
            if len(ips) > 0 {
                clientIP := strings.TrimSpace(ips[0])
                if net.ParseIP(clientIP) != nil {
                    return clientIP
                }
            }
        }

        // Check X-Real-IP header
        if xri := r.Header.Get("X-Real-IP"); xri != "" {
            if net.ParseIP(xri) != nil {
                return xri
            }
        }
    }

    // Use direct connection IP (don't trust headers from untrusted sources)
    return directIP
}

Rate Limiting in Practice

// @filename: main.go
// handleLogin handles admin login with rate limiting
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
    clientIP := s.rateLimiter.GetClientIP(r)

    // Check if IP is blocked
    if s.rateLimiter.IsBlocked(clientIP) {
        until := s.rateLimiter.BlockedUntil(clientIP)
        w.Header().Set("Retry-After", strconv.Itoa(int(time.Until(until).Seconds())))
        http.Error(w, "Too many login attempts. Please try again later.", http.StatusTooManyRequests)
        return
    }

    // Check credentials
    var loginData struct {
        Email    string `json:"email"`
        Password string `json:"password"`
    }
    if err := json.NewDecoder(r.Body).Decode(&loginData); err != nil {
        http.Error(w, "Invalid request", http.StatusBadRequest)
        return
    }

    // Authenticate
    userID, err := s.authenticateUser(loginData.Email, loginData.Password)
    if err != nil {
        // Record failure
        if s.rateLimiter.RecordFailure(clientIP) {
            // IP is now blocked
            w.Header().Set("Retry-After", strconv.Itoa(int(30*60)))
            http.Error(w, "Too many failed login attempts. Blocked for 30 minutes.", http.StatusTooManyRequests)
        } else {
            remaining := s.rateLimiter.RemainingAttempts(clientIP)
            http.Error(w, fmt.Sprintf("Invalid credentials. %d attempts remaining.", remaining), http.StatusUnauthorized)
        }
        return
    }

    // Success - clear rate limit
    s.rateLimiter.RecordSuccess(clientIP)

    // Create session
    token := s.createSession(userID)

    // Set cookie
    http.SetCookie(w, &http.Cookie{
        Name:     "admin_session",
        Value:    token,
        Path:     "/admin",
        HttpOnly: true,
        Secure:   isSecureContext(r),
        SameSite: http.SameSiteStrictMode,
        MaxAge:   86400, // 24 hours
    })
}

CORS Blocking: Explicit Deny for External Requests

Cross-Origin Resource Sharing (CORS) can be a significant security risk if not properly configured. The email-server takes a defensive approach: explicitly deny all cross-origin requests.

// @filename: main.go
func (s *Server) withSecurityHeaders(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // CRITICAL: Block all cross-origin requests - admin panel should NEVER be accessed from external sites
        w.Header().Set("Access-Control-Allow-Origin", "") // Explicitly NO CORS
        w.Header().Set("Access-Control-Allow-Methods", "")
        w.Header().Set("Access-Control-Allow-Headers", "")

        next.ServeHTTP(w, r)
    })
}

Why Block CORS Entirely?

For an admin panel:

  1. No legitimate cross-origin use case: The admin panel should only be accessed directly
  2. Prevents CSRF via CORS: Attackers cannot use CORS to bypass SameSite cookie protections
  3. Stops information leaks: External sites cannot make authenticated requests to the admin panel

When to Allow CORS

CORS should only be enabled for public APIs that are designed to be accessed from external origins:

// @filename: main.go
// Example: For a public API endpoint
if strings.HasPrefix(r.URL.Path, "/api/public/") {
    w.Header().Set("Access-Control-Allow-Origin", "https://trusted-domain.com")
    w.Header().Set("Access-Control-Allow-Methods", "GET, POST")
    w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
}

Referrer Policy: Maximum Privacy

The Referrer-Policy header controls what information is sent in the Referer header when users navigate from your page to another.

w.Header().Set("Referrer-Policy", "no-referrer")

Policy Options

PolicyDescriptionPrivacy Level
no-referrerNever send referrerMaximum
no-referrer-when-downgradeDefault behaviorLow
same-originOnly send for same-originHigh
strict-origin-when-cross-originSend origin onlyMedium
originSend only originHigh

Why no-referrer?

For an email admin panel handling sensitive data:

  1. Prevents information leakage: Sensitive URLs (with query parameters) won’t be sent to external sites
  2. Maximum privacy: No referrer information is ever transmitted
  3. No legitimate use case: Users shouldn’t be navigating from admin panel to external sites

Use Cases for Different Policies

// @filename: main.go
// Admin panel: Maximum privacy
w.Header().Set("Referrer-Policy", "no-referrer")

// Public content: Standard security
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")

// Analytics/tracking: Allow same-origin
w.Header().Set("Referrer-Policy", "same-origin")

Cookies must be configured securely to prevent theft and session hijacking.

Secure Context Detection

The Secure flag should only be set when the request is truly over HTTPS:

// @filename: handlers.go
func isSecureContext(r *http.Request) bool {
    // Direct TLS connection
    if r.TLS != nil {
        return true
    }
    // Check if behind HTTPS proxy
    if proto := r.Header.Get("X-Forwarded-Proto"); proto == "https" {
        return true
    }
    // In production with nginx, always return true
    // This is safer than potentially setting Secure=false
    return true
}
// @filename: main.go
http.SetCookie(w, &http.Cookie{
    Name:     "admin_session",
    Value:    token,
    Path:     "/admin",
    HttpOnly: true,          // Prevent JavaScript access
    Secure:   isSecureContext(r),  // Only send over HTTPS
    SameSite: http.SameSiteStrictMode,  // CSRF protection
    MaxAge:   86400,        // 24 hour expiration
})

Key Cookie Security Attributes:

  1. HttpOnly: Prevents JavaScript from accessing the cookie (XSS protection)
  2. Secure: Only sends cookie over HTTPS
  3. SameSite=Strict: Prevents CSRF attacks by not sending cookies with cross-site requests
  4. Path: Restricts cookie to specific paths
  5. MaxAge: Sets expiration time (no persistent cookies)

Security Headers Summary

HeaderPurposeValue
Content-Security-PolicyRestricts resource loadingdefault-src 'self'
X-Frame-OptionsPrevents clickjackingDENY
X-Content-Type-OptionsPrevents MIME sniffingnosniff
X-XSS-ProtectionXSS protection (legacy)1; mode=block
Referrer-PolicyControls referrer headerno-referrer
Permissions-PolicyControls browser featurescamera=(), geolocation=(), ...
Access-Control-Allow-OriginCORS controlEmpty (block all)

Testing Security Controls

Testing CSRF Protection

# @filename: script.sh
# Test CSRF token validation
curl -X POST http://localhost:8080/admin/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@example.com","password":"wrong"}' \
  -v 2>&1 | grep "403"

# Should return 403 Forbidden without token

# Test with invalid token
curl -X POST http://localhost:8080/admin/login \
  -H "Content-Type: application/json" \
  -H "X-CSRF-Token: invalid-token-123" \
  -d '{"email":"admin@example.com","password":"wrong"}' \
  -v 2>&1 | grep "403"

Testing CSP

# @filename: script.sh
# Check CSP header
curl -I http://localhost:8080/admin/ 2>&1 | grep -i "content-security-policy"

# Expected output:
# Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; ...

# Test inline script blocking (if configured)
# Create a test page with inline script and try to inject

Testing Rate Limiting

# @filename: script.sh
# Test rate limiting
for i in {1..10}; do
  curl -X POST http://localhost:8080/admin/login \
    -H "Content-Type: application/json" \
    -d '{"email":"test@example.com","password":"wrong"}' \
    -w "\nStatus: %{http_code}\n" \
    -o /dev/null
done

# Should see 429 Too Many Requests after 5 attempts

Testing CORS Blocking

# @filename: script.sh
# Test CORS headers
curl -I http://localhost:8080/admin/ -H "Origin: https://evil.com" 2>&1 | grep -i "access-control"

# Should show empty Access-Control-Allow-Origin header

# Test preflight request
curl -X OPTIONS http://localhost:8080/admin/ \
  -H "Origin: https://evil.com" \
  -H "Access-Control-Request-Method: POST" \
  -v 2>&1 | grep -i "access-control"

Testing Security Headers

# @filename: script.sh
# Check all security headers
curl -I http://localhost:8080/admin/ 2>&1 | grep -E "(X-Frame|X-Content|Referrer|Permissions|Content-Security)"

# Expected output:
# X-Frame-Options: DENY
# X-Content-Type-Options: nosniff
# Referrer-Policy: no-referrer
# Permissions-Policy: accelerometer=(), camera=(), ...
# Content-Security-Policy: default-src 'self'; ...

Automated Testing with OWASP ZAP

# @filename: script.sh
# Run OWASP ZAP security scan
docker run -t owasp/zap2docker-stable zap-baseline.py \
  -t http://localhost:8080/admin/ \
  -r report.html

# Review the report for:
# - Missing security headers
# - CSRF vulnerabilities
# - CSP violations
# - Information disclosure

Security Best Practices Summary

CSRF Protection

  1. Use cryptographically secure random tokens
  2. Allow token reuse until expiration for multi-step workflows
  3. Validate tokens on all state-changing requests
  4. Include tokens in AJAX requests via headers
  5. Implement periodic token cleanup

CSP Implementation

  1. Use object-src 'none' to block plugins
  2. Block FLoC with interest-cohort=() in Permissions Policy
  3. Extract inline scripts to separate files when possible
  4. Use nonces or hashes instead of unsafe-inline in production
  5. Include frame-ancestors 'none' to prevent clickjacking

Rate Limiting

  1. Implement per-IP and per-user limits
  2. Use trusted proxy lists for accurate client IP detection
  3. Implement periodic cleanup to prevent memory leaks
  4. Block IPs after exceeding limits
  5. Provide clear error messages with retry information

CORS Configuration

  1. Default to denying all cross-origin requests
  2. Only enable CORS for explicitly allowed origins
  3. Restrict allowed methods and headers
  4. Never use * in production

Referrer Policy

  1. Use no-referrer for admin panels and sensitive areas
  2. Use strict-origin-when-cross-origin for public content
  3. Never send sensitive data in referrer URLs
  1. Always set HttpOnly flag
  2. Set Secure flag only on HTTPS
  3. Use SameSite=Strict for maximum CSRF protection
  4. Set appropriate expiration times
  5. Restrict cookies to specific paths

Conclusion

Defensive security requires multiple layers of protection. The email-server project demonstrates how CSRF protection, CSP hardening, rate limiting, CORS blocking, and security headers work together to create a secure application.

The key lessons learned:

  1. Security is iterative: Issues like CSRF token handling emerge through real-world usage
  2. Defense in depth: Multiple security measures compensate for potential weaknesses
  3. Memory management matters: Even security features can introduce vulnerabilities (memory leaks)
  4. Proxy-aware design: Rate limiting must work correctly behind reverse proxies
  5. Default deny: Explicitly deny rather than implicitly allow for maximum security

By implementing these defensive security measures, you can significantly reduce the attack surface of your web applications and protect against common vulnerabilities.

security go csrf csp rate-limiting cors web-security
Share:

Continue Reading

Two-Factor Authentication & Admin Security

A comprehensive guide to implementing TOTP-based two-factor authentication in production systems. Covers server-side QR code generation, secure cookie handling with proxy detection, trusted device management, session duration optimization, and audit logging for compliance. Learn from real implementation in the email-server project with code examples from Go.

Read article
security2fatotp

Implementing Two-Factor Authentication (2FA) in Node.js with TOTP and Google Authenticator

Two-Factor Authentication (2FA) is an extra layer of security that requires not only a password and username but also something that only the user has on them, typically a one-time code generated by an app like Google Authenticator. This guide walks you through implementing 2FA in a Node.js application using TOTP, Google Authenticator, and otplib.

Read article
Node.jsJavaScriptBackend