CORS

CORS #

Cross-Origin Resource Sharing (CORS) adalah mekanisme keamanan browser yang membatasi halaman web dari membuat request ke domain yang berbeda dari domain halaman itu sendiri. Ketika frontend di app.example.com memanggil API di api.example.com, browser akan memeriksa apakah server API mengizinkan request lintas domain tersebut.

Caddy tidak memiliki direktif cors bawaan, tapi konfigurasi CORS bisa dilakukan sepenuhnya menggunakan direktif header dan matcher — dengan kontrol penuh atas setiap header yang diperlukan.

Mengapa CORS Ada #

Tanpa CORS (same-origin policy saja):
  frontend di app.example.com TIDAK bisa memanggil api.example.com
  → Browser menolak request sebelum sampai ke server

Dengan CORS — server menyatakan: "Saya izinkan request dari origin ini":
  api.example.com mengirim header:
  Access-Control-Allow-Origin: https://app.example.com
  
  → Browser melihat header ini dan mengizinkan response dibaca oleh frontend

Preflight Request #

Untuk request yang dianggap “non-simple” (metode PUT/DELETE, atau header kustom), browser mengirim OPTIONS preflight request terlebih dahulu:

1. Frontend mau kirim: PUT /api/users/123
   Dengan header:      Authorization: Bearer token
   
2. Browser kirim OPTIONS dulu (preflight):
   OPTIONS /api/users/123 HTTP/1.1
   Origin: https://app.example.com
   Access-Control-Request-Method: PUT
   Access-Control-Request-Headers: Authorization
   
3. Server harus respond 204/200 dengan:
   Access-Control-Allow-Origin: https://app.example.com
   Access-Control-Allow-Methods: PUT, GET, POST, DELETE
   Access-Control-Allow-Headers: Authorization, Content-Type
   Access-Control-Max-Age: 86400
   
4. Baru setelah itu, browser kirim request aslinya:
   PUT /api/users/123 HTTP/1.1
   Authorization: Bearer token

Konfigurasi CORS Dasar #

api.example.com {
    @options method OPTIONS
    
    # Handle preflight request
    handle @options {
        header Access-Control-Allow-Origin      "https://app.example.com"
        header Access-Control-Allow-Methods     "GET, POST, PUT, DELETE, PATCH, OPTIONS"
        header Access-Control-Allow-Headers     "Content-Type, Authorization, X-Requested-With"
        header Access-Control-Max-Age           "86400"
        respond "" 204
    }
    
    # Header CORS untuk request biasa
    header Access-Control-Allow-Origin  "https://app.example.com"
    header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, OPTIONS"
    
    reverse_proxy backend:8080
}

CORS dengan Kredensial (Cookies / Authorization) #

Ketika request menyertakan credentials (cookies, Authorization header), konfigurasi CORS memerlukan aturan tambahan:

api.example.com {
    @options method OPTIONS
    
    handle @options {
        # WAJIB: wildcard (*) tidak bisa digunakan dengan credentials
        header Access-Control-Allow-Origin      "https://app.example.com"
        header Access-Control-Allow-Methods     "GET, POST, PUT, DELETE, PATCH, OPTIONS"
        header Access-Control-Allow-Headers     "Content-Type, Authorization, Cookie"
        # WAJIB: izinkan kredensial
        header Access-Control-Allow-Credentials "true"
        header Access-Control-Max-Age           "86400"
        respond "" 204
    }
    
    header Access-Control-Allow-Origin      "https://app.example.com"
    header Access-Control-Allow-Credentials "true"
    # Expose header yang bisa dibaca frontend
    header Access-Control-Expose-Headers    "X-Total-Count, X-Page, X-Per-Page"
    
    reverse_proxy backend:8080
}

Multiple Origins yang Diizinkan #

Caddy tidak mendukung multiple origins secara langsung di satu header (spec HTTP hanya mengizinkan satu nilai). Solusinya adalah memeriksa header Origin dari request dan menyetnya secara dinamis:

api.example.com {
    @allowedOrigin {
        header Origin "https://app.example.com"
        header Origin "https://admin.example.com"
        header Origin "https://mobile.example.com"
    }
    
    @options method OPTIONS
    
    handle @options {
        header Access-Control-Allow-Origin      "{header.Origin}"
        header Vary                             "Origin"
        header Access-Control-Allow-Methods     "GET, POST, PUT, DELETE, PATCH, OPTIONS"
        header Access-Control-Allow-Headers     "Content-Type, Authorization"
        header Access-Control-Allow-Credentials "true"
        header Access-Control-Max-Age           "86400"
        respond "" 204
    }
    
    handle @allowedOrigin {
        header Access-Control-Allow-Origin      "{header.Origin}"
        header Vary                             "Origin"
        header Access-Control-Allow-Credentials "true"
        reverse_proxy backend:8080
    }
    
    # Untuk origin yang tidak diizinkan, tidak tambahkan CORS headers
    handle {
        reverse_proxy backend:8080
    }
}

CORS untuk API Publik (Semua Origin) #

Untuk API yang memang publik dan bisa diakses dari domain mana pun:

public-api.example.com {
    @options method OPTIONS
    
    handle @options {
        header Access-Control-Allow-Origin  "*"
        header Access-Control-Allow-Methods "GET, OPTIONS"
        header Access-Control-Allow-Headers "Content-Type, Authorization"
        header Access-Control-Max-Age       "86400"
        respond "" 204
    }
    
    # Untuk API publik: wildcard OK karena tidak ada kredensial
    header Access-Control-Allow-Origin  "*"
    header Access-Control-Allow-Methods "GET, OPTIONS"
    
    reverse_proxy backend:8080
}
Access-Control-Allow-Origin: * tidak bisa dikombinasikan dengan Access-Control-Allow-Credentials: true. Jika API memerlukan credentials (cookies, JWT), kamu harus menentukan origin secara eksplisit.

Snippet CORS yang Dapat Digunakan Ulang #

# Snippet untuk CORS ke domain spesifik
(cors_for_app) {
    @options method OPTIONS
    handle @options {
        header Access-Control-Allow-Origin      "https://app.example.com"
        header Access-Control-Allow-Methods     "GET, POST, PUT, DELETE, PATCH, OPTIONS"
        header Access-Control-Allow-Headers     "Content-Type, Authorization, X-Requested-With"
        header Access-Control-Allow-Credentials "true"
        header Access-Control-Max-Age           "86400"
        respond "" 204
    }
    header Access-Control-Allow-Origin      "https://app.example.com"
    header Access-Control-Allow-Credentials "true"
    header Access-Control-Expose-Headers    "X-Total-Count"
}

api.example.com {
    import cors_for_app
    reverse_proxy backend:8080
}

api-v2.example.com {
    import cors_for_app
    reverse_proxy backend-v2:8080
}

CORS dalam Arsitektur Microservices #

example.com {
    # CORS hanya dikonfigurasi di edge (Caddy)
    # Microservice backend tidak perlu handle CORS sendiri
    
    @options method OPTIONS
    
    handle @options {
        header Access-Control-Allow-Origin      "https://frontend.example.com"
        header Access-Control-Allow-Methods     "GET, POST, PUT, DELETE, PATCH, OPTIONS"
        header Access-Control-Allow-Headers     "Content-Type, Authorization"
        header Access-Control-Allow-Credentials "true"
        header Access-Control-Max-Age           "86400"
        respond "" 204
    }
    
    header Access-Control-Allow-Origin      "https://frontend.example.com"
    header Access-Control-Allow-Credentials "true"
    
    # Routing ke berbagai microservice
    handle /api/users/* {
        reverse_proxy user-service:3001
    }
    handle /api/orders/* {
        reverse_proxy order-service:3002
    }
    handle /api/products/* {
        reverse_proxy product-service:3003
    }
}

Debugging CORS Errors #

# Simulasi preflight request
curl -sv -X OPTIONS https://api.example.com/users \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: GET" \
  -H "Access-Control-Request-Headers: Authorization" \
  2>&1 | grep -E "< Access-Control|< HTTP"

# Cek apakah CORS headers ada di response biasa
curl -I https://api.example.com/users \
  -H "Origin: https://app.example.com" | grep -i "access-control"

# Output yang diharapkan:
# access-control-allow-origin: https://app.example.com
# access-control-allow-credentials: true
# vary: Origin

# Error umum dan solusinya:
# "No 'Access-Control-Allow-Origin' header"
#   → CORS belum dikonfigurasi atau origin tidak cocok

# "has been blocked by CORS policy: The value of 'Access-Control-Allow-Origin'
#  header in the response must not be the wildcard '*' when the request's
#  credentials mode is 'include'"
#   → Gunakan origin eksplisit jika pakai credentials

# "Response to preflight request doesn't pass access control check"
#   → Cek konfigurasi preflight (OPTIONS handler)

Ringkasan #

  • CORS dikonfigurasi di Caddy menggunakan header direktif dan matcher method OPTIONS untuk preflight — tidak ada direktif cors bawaan.
  • Preflight OPTIONS request harus ditangani secara eksplisit dan mengembalikan status 204 dengan CORS headers yang lengkap.
  • Access-Control-Allow-Origin: * (wildcard) tidak bisa dikombinasikan dengan Access-Control-Allow-Credentials: true — gunakan origin eksplisit jika request menyertakan credentials.
  • Untuk multiple origins, gunakan {header.Origin} untuk echo kembali origin yang dikirim client (setelah memvalidasi apakah origin tersebut diizinkan).
  • Tambahkan header Vary: Origin saat menggunakan origin dinamis agar CDN/proxy tidak salah meng-cache response untuk origin berbeda.
  • Dalam arsitektur microservices, konfigurasikan CORS hanya di layer Caddy (edge) — service backend tidak perlu handle CORS sendiri, menyederhanakan implementasi.

← Sebelumnya: Security Headers   Berikutnya: Rewrite →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact