🔐 Security✍️ Khoa📅 19/04/2026☕ 9 phút đọc

API & Web Security

APIs là giao diện chính giữa services và users trong modern applications. Securing APIs không chỉ là add authentication — nó cover rate limiting, input validation, versioning, và nhiều layers khác.

🌐 Reality check: "Your API is only as secure as its weakest endpoint."


API Authentication Methods

1. API Keys

Cách đơn giản nhất — client gửi static key với mỗi request.

GET /api/users HTTP/1.1
Host: api.example.com
X-API-Key: sk_live_abc123xyz456

Implementation:

type APIKey struct {
    Key       string
    UserID    int64
    CreatedAt time.Time
    LastUsed  time.Time
}

func apiKeyMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        apiKey := r.Header.Get("X-API-Key")
        if apiKey == "" {
            http.Error(w, "Missing API key", 401)
            return
        }

        // Verify key (check DB or cache)
        key, err := db.GetAPIKey(apiKey)
        if err != nil {
            http.Error(w, "Invalid API key", 401)
            return
        }

        // Update last used
        db.UpdateAPIKeyLastUsed(apiKey)

        // Attach user to context
        ctx := context.WithValue(r.Context(), "userID", key.UserID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Pros:

  • ✅ Simple to implement
  • ✅ Good for server-to-server

Cons:

  • ❌ If leaked, valid until manually revoked
  • ❌ No expiration
  • ❌ Hard to rotate

Best practices:

  • ✅ Use HTTPS only (keys in plaintext)
  • ✅ Prefix keys with identifier (e.g., sk_live_, pk_test_)
  • ✅ Store hashed version in DB
  • ✅ Allow multiple keys per user (for rotation)

2. Bearer Tokens (JWT)

JWT đã covered trong auth/ — here's API-specific usage:

GET /api/users HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Implementation:

func jwtAPIMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        authHeader := r.Header.Get("Authorization")
        if !strings.HasPrefix(authHeader, "Bearer ") {
            http.Error(w, "Missing bearer token", 401)
            return
        }

        tokenString := strings.TrimPrefix(authHeader, "Bearer ")
        claims, err := validateJWT(tokenString)
        if err != nil {
            http.Error(w, "Invalid token", 401)
            return
        }

        // Check scopes
        if !hasScope(claims.Scopes, "api:read") {
            http.Error(w, "Insufficient permissions", 403)
            return
        }

        ctx := context.WithValue(r.Context(), "claims", claims)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

3. HMAC Signatures

Client signs request with secret key → server verifies signature.

Flow:

Client has: API_KEY_ID + API_SECRET

1. Build canonical request string:
   METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + BODY_HASH

2. Sign with HMAC-SHA256:
   signature = HMAC-SHA256(canonical_request, API_SECRET)

3. Send with headers:
   X-API-Key-ID: key_abc123
   X-Timestamp: 1234567890
   X-Signature: computed_signature

Implementation:

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)

// Client-side: Generate signature
func signRequest(method, path string, timestamp int64, body []byte, secret string) string {
    bodyHash := sha256.Sum256(body)
    canonical := fmt.Sprintf("%s\n%s\n%d\n%x", method, path, timestamp, bodyHash)

    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(canonical))
    return hex.EncodeToString(mac.Sum(nil))
}

// Server-side: Verify signature
func verifyHMACMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        keyID := r.Header.Get("X-API-Key-ID")
        timestamp := r.Header.Get("X-Timestamp")
        receivedSig := r.Header.Get("X-Signature")

        if keyID == "" || timestamp == "" || receivedSig == "" {
            http.Error(w, "Missing authentication headers", 401)
            return
        }

        // Check timestamp (防止 replay attacks)
        ts, _ := strconv.ParseInt(timestamp, 10, 64)
        if abs(time.Now().Unix()-ts) > 300 { // 5 minutes tolerance
            http.Error(w, "Request too old", 401)
            return
        }

        // Get secret from DB
        secret, err := db.GetAPISecret(keyID)
        if err != nil {
            http.Error(w, "Invalid API key", 401)
            return
        }

        // Read body (need to buffer for verification)
        body, _ := io.ReadAll(r.Body)
        r.Body = io.NopCloser(bytes.NewBuffer(body))

        // Compute expected signature
        expectedSig := signRequest(r.Method, r.URL.Path, ts, body, secret)

        // Constant-time comparison (prevent timing attacks)
        if !hmac.Equal([]byte(receivedSig), []byte(expectedSig)) {
            http.Error(w, "Invalid signature", 401)
            return
        }

        next.ServeHTTP(w, r)
    })
}

Pros:

  • ✅ Request integrity (cannot tamper)
  • ✅ Replay protection (timestamp validation)
  • ✅ No token expiry needed

Cons:

  • ❌ More complex than API keys
  • ❌ Client & server clock must be synchronized

4. OAuth 2.0 (Third-party Access)

Covered in auth/oauth2-and-oidc.md.

Use case: User grants third-party app access to their data.


API Rate Limiting

Prevent abuse, ensure fair usage, protect against DDoS.

Strategy 1: Token Bucket

import "golang.org/x/time/rate"

type RateLimiter struct {
    limiters map[string]*rate.Limiter
    mu       sync.RWMutex
    rate     rate.Limit
    burst    int
}

func NewRateLimiter(r rate.Limit, b int) *RateLimiter {
    return &RateLimiter{
        limiters: make(map[string]*rate.Limiter),
        rate:     r,
        burst:    b,
    }
}

func (rl *RateLimiter) GetLimiter(key string) *rate.Limiter {
    rl.mu.RLock()
    limiter, exists := rl.limiters[key]
    rl.mu.RUnlock()

    if exists {
        return limiter
    }

    rl.mu.Lock()
    defer rl.mu.Unlock()

    limiter = rate.NewLimiter(rl.rate, rl.burst)
    rl.limiters[key] = limiter
    return limiter
}

// Middleware
var apiLimiter = NewRateLimiter(rate.Limit(100), 200)
// 100 requests/second, burst of 200

func rateLimitMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Rate limit by API key or IP
        apiKey := r.Header.Get("X-API-Key")
        if apiKey == "" {
            apiKey = r.RemoteAddr
        }

        limiter := apiLimiter.GetLimiter(apiKey)
        if !limiter.Allow() {
            w.Header().Set("Retry-After", "60")
            http.Error(w, "Rate limit exceeded", 429)
            return
        }

        next.ServeHTTP(w, r)
    })
}

Strategy 2: Sliding Window (Redis)

func checkRateLimitRedis(key string, limit int, window time.Duration) (bool, error) {
    ctx := context.Background()
    now := time.Now().UnixMilli()
    windowStart := now - window.Milliseconds()

    pipe := rdb.Pipeline()

    // Remove old entries
    pipe.ZRemRangeByScore(ctx, "ratelimit:"+key, "0", strconv.FormatInt(windowStart, 10))

    // Count requests in window
    pipe.ZCard(ctx, "ratelimit:"+key)

    // Add current request
    pipe.ZAdd(ctx, "ratelimit:"+key, redis.Z{Score: float64(now), Member: now})

    // Set expiry
    pipe.Expire(ctx, "ratelimit:"+key, window)

    results, err := pipe.Exec(ctx)
    if err != nil {
        return false, err
    }

    count := results[1].(*redis.IntCmd).Val()
    return count < int64(limit), nil
}

// Usage
allowed, _ := checkRateLimitRedis("user:123", 100, 1*time.Minute)
if !allowed {
    http.Error(w, "Rate limit exceeded", 429)
    return
}

Rate Limit Headers

func setRateLimitHeaders(w http.ResponseWriter, limit, remaining int, reset time.Time) {
    w.Header().Set("X-RateLimit-Limit", strconv.Itoa(limit))
    w.Header().Set("X-RateLimit-Remaining", strconv.Itoa(remaining))
    w.Header().Set("X-RateLimit-Reset", strconv.FormatInt(reset.Unix(), 10))
}

// Example
setRateLimitHeaders(w, 100, 73, time.Now().Add(1*time.Minute))

Response:

HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1234567890

CORS (Cross-Origin Resource Sharing)

Browser security policy: JavaScript from domain-a.com cannot call API at domain-b.com unless allowed.

CORS Preflight Request

Browser                               Server (api.example.com)
  │                                         │
  │ ① OPTIONS /api/users                    │
  │    Origin: https://app.example.com      │
  │    Access-Control-Request-Method: POST  │
  ├────────────────────────────────────────►│
  │                                         │
  │              ② Check if origin allowed  │
  │                                         │
  │ ③ 200 OK                                │
  │    Access-Control-Allow-Origin: *       │
  │    Access-Control-Allow-Methods: GET,POST
  │◄────────────────────────────────────────┤
  │                                         │
  │ ④ Actual request: POST /api/users       │
  ├────────────────────────────────────────►│
  │                                         │

CORS Implementation

func corsMiddleware(allowedOrigins []string) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            origin := r.Header.Get("Origin")

            // Check if origin is allowed
            allowed := false
            for _, o := range allowedOrigins {
                if o == "*" || o == origin {
                    allowed = true
                    break
                }
            }

            if !allowed {
                http.Error(w, "Origin not allowed", 403)
                return
            }

            // Set CORS headers
            w.Header().Set("Access-Control-Allow-Origin", origin)
            w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
            w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key")
            w.Header().Set("Access-Control-Allow-Credentials", "true")
            w.Header().Set("Access-Control-Max-Age", "86400") // Cache preflight for 24h

            // Handle preflight
            if r.Method == "OPTIONS" {
                w.WriteHeader(204)
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

// Usage
http.Handle("/api/", corsMiddleware([]string{
    "https://app.example.com",
    "https://admin.example.com",
})(apiHandler))

Security notes:

  • Never use Access-Control-Allow-Origin: * with credentials
  • ✅ Whitelist specific origins
  • ✅ Don't reflect Origin header without validation

Input Validation for APIs

JSON Input

type CreateUserRequest struct {
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=8"`
    Age      int    `json:"age" validate:"required,min=0,max=150"`
}

func createUserHandler(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    
    // 1. Parse JSON
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "Invalid JSON", 400)
        return
    }

    // 2. Validate
    validate := validator.New()
    if err := validate.Struct(req); err != nil {
        http.Error(w, err.Error(), 400)
        return
    }

    // 3. Additional business logic validation
    if isEmailTaken(req.Email) {
        http.Error(w, "Email already exists", 409)
        return
    }

    // 4. Process request...
}

Query Parameters

func getUsersHandler(w http.ResponseWriter, r *http.Request) {
    // Validate page number
    page := r.URL.Query().Get("page")
    pageNum, err := strconv.Atoi(page)
    if err != nil || pageNum < 1 {
        pageNum = 1
    }

    // Validate limit
    limit := r.URL.Query().Get("limit")
    limitNum, err := strconv.Atoi(limit)
    if err != nil || limitNum < 1 || limitNum > 100 {
        limitNum = 20 // Default
    }

    // Validate sort field (whitelist)
    sortField := r.URL.Query().Get("sort")
    allowedSorts := map[string]bool{"name": true, "created_at": true, "email": true}
    if !allowedSorts[sortField] {
        sortField = "created_at" // Default
    }

    users := getUsers(pageNum, limitNum, sortField)
    json.NewEncoder(w).Encode(users)
}

API Versioning

URL Versioning

router := chi.NewRouter()

// Version 1
router.Group(func(r chi.Router) {
    r.Route("/v1/users", func(r chi.Router) {
        r.Get("/", listUsersV1)
        r.Post("/", createUserV1)
    })
})

// Version 2
router.Group(func(r chi.Router) {
    r.Route("/v2/users", func(r chi.Router) {
        r.Get("/", listUsersV2)
        r.Post("/", createUserV2)
    })
})

Header Versioning

func versionMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        version := r.Header.Get("API-Version")
        if version == "" {
            version = "v1" // Default
        }

        ctx := context.WithValue(r.Context(), "api_version", version)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Pros/Cons:

Method Pros Cons
URL ✅ Clear, cacheable ❌ URL duplication
Header ✅ Clean URLs ❌ Harder to test in browser
Query param ✅ Simple ❌ Not RESTful

GraphQL Security

Query Depth Limiting

import "github.com/99designs/gqlgen"

func main() {
    srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &resolver{}}))

    // Limit query depth
    srv.AroundFields(func(ctx context.Context, next graphql.Resolver) (interface{}, error) {
        fc := graphql.GetFieldContext(ctx)
        if fc.Field.SelectionSet != nil && len(fc.Field.SelectionSet) > 10 {
            return nil, errors.New("query too complex")
        }
        return next(ctx)
    })
}

Query Cost Analysis

# 🔥 Expensive query
query {
  users {           # 1000 users
    posts {         # 100 posts each
      comments {    # 50 comments each
        author {    # 1 author each
          friends { # 200 friends each
            ...     # → 1000 * 100 * 50 * 200 = 1B operations!
          }
        }
      }
    }
  }
}

Solution: Calculate query cost before execution.

func calculateQueryCost(query ast.SelectionSet, maxDepth int, currentDepth int) int {
    if currentDepth > maxDepth {
        return math.MaxInt32 // Too deep
    }

    cost := 0
    for _, selection := range query {
        if field, ok := selection.(*ast.Field); ok {
            cost += 1
            if field.SelectionSet != nil {
                cost += calculateQueryCost(field.SelectionSet, maxDepth, currentDepth+1)
            }
        }
    }
    return cost
}

API Security Checklist

Authentication & Authorization:

  • Use HTTPS only
  • Implement proper authentication (JWT, API keys, OAuth)
  • Validate tokens/signatures on every request
  • Implement authorization (RBAC/ABAC)
  • Don't expose internal IDs (use UUIDs)

Input Validation:

  • Validate all inputs (JSON, query params, headers)
  • Use whitelists for enums/sort fields
  • Limit request body size
  • Sanitize user-generated content

Rate Limiting:

  • Implement rate limiting per user/IP
  • Different limits for different endpoints
  • Return proper 429 status with Retry-After

CORS:

  • Whitelist allowed origins
  • Don't use * with credentials
  • Set proper allowed methods/headers

Error Handling:

  • Don't leak stack traces
  • Generic error messages to clients
  • Detailed logs internally

Versioning:

  • Support API versioning
  • Deprecation policy & timeline
  • Backward compatibility

Monitoring:

  • Log authentication failures
  • Monitor rate limit violations
  • Alert on suspicious patterns

Tóm tắt

Aspect Best Practice
Authentication JWT or HMAC signatures
Rate limiting Token bucket or sliding window
CORS Whitelist origins, not *
Input validation Whitelist + type checking
Versioning URL-based (/v1/, /v2/)
Error handling Generic messages, detailed logs
GraphQL Depth & complexity limits

Golden rules:

  • Always use HTTPS
  • Validate everything
  • Rate limit everything
  • Log security events
  • Fail securely

Bước tiếp theo

  • auth/oauth2-and-oidc.md — OAuth for third-party API access
  • distributed-systems-security.md — Service-to-service authentication
  • interview-and-big-picture.md — API security interview questions