Day 75 — API Authentication Patterns
Learning Objectives
- Implement three auth patterns as middleware: API key, HMAC signed request, ECDSA token
- Return timing-consistent responses (prevent timing attacks on auth)
- Understand when to use each pattern
Auth Pattern Decision Matrix
| Pattern | Use When | Security Model |
|---|---|---|
| API Key header | Simple service-to-service, low risk | Secret string in header |
| HMAC signed request | Webhook validation, AWS-style auth | Shared secret, covers body hash |
| ECDSA token (JWT-like) | User authentication, expiring tokens | Asymmetric, can verify without secret |
| mTLS certificate | Zero-trust service mesh | PKI, strongest |
Exercise: Three Auth Middlewares
package auth
import (
"context"
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"fmt"
"net/http"
"strings"
"time"
)
type ctxKey string
const (
subjectKey ctxKey = "auth_subject"
methodKey ctxKey = "auth_method"
)
func GetSubject(ctx context.Context) string {
s, _ := ctx.Value(subjectKey).(string)
return s
}
// ── Auth Pattern 1: API Key ──────────────────────────────────────────────────
type APIKeyStore interface {
Lookup(key string) (subject string, ok bool)
}
type MemAPIKeyStore struct {
keys map[string]string // key → subject
}
func (s *MemAPIKeyStore) Lookup(key string) (string, bool) {
sub, ok := s.keys[key]
return sub, ok
}
func APIKeyAuth(store APIKeyStore) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("X-API-Key")
if key == "" {
// Also check Authorization: Bearer <key>
auth := r.Header.Get("Authorization")
if strings.HasPrefix(auth, "Bearer ") {
key = auth[7:]
}
}
if key == "" {
denyAuth(w, r, "missing api key")
return
}
subject, ok := store.Lookup(key)
if !ok {
// Add artificial delay to prevent timing enumeration
// (all invalid keys take the same time)
time.Sleep(5 * time.Millisecond)
denyAuth(w, r, "invalid api key")
return
}
ctx := context.WithValue(r.Context(), subjectKey, subject)
ctx = context.WithValue(ctx, methodKey, "api_key")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// ── Auth Pattern 2: HMAC Request Signing (AWS SigV4 style) ─────────────────
type HMACAuth struct {
keys map[string][]byte // key-id → secret
}
func (h *HMACAuth) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Expected header: Authorization: HMAC-SHA256 key-id=xxx,timestamp=xxx,signature=xxx
authHeader := r.Header.Get("Authorization")
if !strings.HasPrefix(authHeader, "HMAC-SHA256 ") {
denyAuth(w, r, "missing HMAC authorization")
return
}
params := parseHMACParams(authHeader[12:])
keyID := params["key-id"]
tsStr := params["timestamp"]
signature := params["signature"]
if keyID == "" || tsStr == "" || signature == "" {
denyAuth(w, r, "malformed HMAC authorization")
return
}
// Check timestamp freshness (replay attack prevention)
ts, err := strconv.ParseInt(tsStr, 10, 64)
if err != nil || abs(time.Now().Unix()-ts) > 300 {
denyAuth(w, r, "timestamp out of window")
return
}
secret, ok := h.keys[keyID]
if !ok {
time.Sleep(5 * time.Millisecond) // constant time
denyAuth(w, r, "unknown key id")
return
}
// Compute expected signature
// Sign: method + "\n" + path + "\n" + timestamp + "\n" + body-hash
bodyHash := sha256BodyHash(r)
signingString := fmt.Sprintf("%s\n%s\n%s\n%s",
r.Method, r.URL.Path, tsStr, bodyHash)
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(signingString))
expected := hex.EncodeToString(mac.Sum(nil))
if subtle.ConstantTimeCompare([]byte(expected), []byte(signature)) != 1 {
denyAuth(w, r, "signature mismatch")
return
}
ctx := context.WithValue(r.Context(), subjectKey, "key:"+keyID)
ctx = context.WithValue(ctx, methodKey, "hmac")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func sha256BodyHash(r *http.Request) string {
if r.Body == nil { return hex.EncodeToString(sha256.New().Sum(nil)) }
body, _ := io.ReadAll(io.LimitReader(r.Body, 1<<20))
restoreBody(r, body)
h := sha256.Sum256(body)
return hex.EncodeToString(h[:])
}
// ── Auth Pattern 3: ECDSA Token (from Day 44) ───────────────────────────────
func ECDSATokenAuth(svc *TokenService) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") {
denyAuth(w, r, "missing bearer token")
return
}
token := auth[7:]
claims, err := svc.Verify(token)
if err != nil {
denyAuth(w, r, "invalid token")
return
}
ctx := context.WithValue(r.Context(), subjectKey, claims.Subject)
ctx = context.WithValue(ctx, methodKey, "ecdsa_token")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func denyAuth(w http.ResponseWriter, r *http.Request, reason string) {
// Audit log the denial
fmt.Printf("[AUTH] denied: %s from %s path=%s\n", reason, r.RemoteAddr, r.URL.Path)
// Return 401 — NEVER leak the specific reason in the response body
w.Header().Set("Content-Type", "application/json")
w.Header().Set("WWW-Authenticate", "Bearer")
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintf(w, `{"error":"authentication required"}`)
}
Real-World Context
The 5ms artificial delay in API key rejection prevents timing enumeration — without it, an attacker can tell if a key prefix is valid by measuring response time. The HMAC 5-minute timestamp window prevents replay attacks (reusing captured requests). These aren't theoretical — they're standard requirements in AWS, Stripe, and GitHub's API auth designs.