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
Originheader 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 accessdistributed-systems-security.md— Service-to-service authenticationinterview-and-big-picture.md— API security interview questions