Frontend Labs
⋯

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.


Generated from week11.md · 8 Oct 2026 16:36