Skip to content

CORS at the API Gateway — Cross-Origin Resource Sharing Configuration

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you'll learn about CORS Gateway. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Cross-Origin Resource Sharing configuration at the gateway controls which web origins can access your API, preventing unauthorized cross-origin requests from browsers.

What You'll Learn

By the end of this lesson, you will configure CORS origins, methods, and headers at the gateway, handle preflight OPTIONS requests, support credentials, and implement per-route CORS policies.

Why It Matters

Improper CORS configuration is a common security vulnerability. The gateway is the ideal central point to enforce consistent CORS policies across all backend services.

Real-World Use

Durga Antivirus Pro configures CORS at the gateway to allow requests only from https://app.durga-antivirus.com and https://dashboard.dodatech.com, rejecting all other origins.

CORS Flow

sequenceDiagram
    Browser->>Gateway: OPTIONS /api/scan (Preflight)
    Gateway->>Gateway: Check Origin
    Gateway->>Browser: 200 + CORS Headers
    Browser->>Gateway: GET /api/scan (Actual Request)
    Gateway->>Gateway: Validate Origin
    Gateway->>Gateway: Add CORS Headers
    Gateway->>Backend: Forward Request
    Backend->>Browser: Response + Vary: Origin

CORS Configuration Engine

A flexible CORS configuration engine for the gateway.

from typing import Dict, List, Optional, Tuple
import re

class CORSConfig:
    def __init__(self):
        self.allowed_origins: List[str] = []
        self.allowed_methods: List[str] = [
            "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"
        ]
        self.allowed_headers: List[str] = [
            "Content-Type", "Authorization", "X-Requested-With"
        ]
        self.exposed_headers: List[str] = []
        self.max_age: int = 86400
        self.allow_credentials: bool = True

    def add_origin(self, origin: str):
        self.allowed_origins.append(origin)

    def is_origin_allowed(self, origin: str) -> bool:
        if "*" in self.allowed_origins:
            return True
        for allowed in self.allowed_origins:
            if allowed == origin:
                return True
            if self._is_wildcard_match(allowed, origin):
                return True
        return False

    def _is_wildcard_match(self, pattern: str,
                           origin: str) -> bool:
        if pattern.startswith("https://*."):
            domain = pattern.replace("https://*.", "")
            return origin.endswith(f".{domain}") and \
                   origin.startswith("https://")
        return False

    def build_preflight_response(self, origin: str
                                 ) -> Tuple[int, Dict]:
        if not self.is_origin_allowed(origin):
            return 403, {}
        headers = {
            "Access-Control-Allow-Origin": origin,
            "Access-Control-Allow-Methods":
                ", ".join(self.allowed_methods),
            "Access-Control-Allow-Headers":
                ", ".join(self.allowed_headers),
            "Access-Control-Max-Age": str(self.max_age),
            "Vary": "Origin",
        }
        if self.allow_credentials:
            headers["Access-Control-Allow-Credentials"] = "true"
        return 200, headers

    def build_cors_headers(self, origin: str) -> Dict:
        if not self.is_origin_allowed(origin):
            return {}
        headers = {
            "Access-Control-Allow-Origin": origin,
            "Vary": "Origin",
        }
        if self.allow_credentials:
            headers["Access-Control-Allow-Credentials"] = "true"
        if self.exposed_headers:
            headers["Access-Control-Expose-Headers"] = \
                ", ".join(self.exposed_headers)
        return headers

config = CORSConfig()
config.add_origin("https://app.example.com")
config.add_origin("https://*.dodatech.com")

for origin in ["https://app.example.com",
                "https://dashboard.dodatech.com",
                "https://evil.com"]:
    status, headers = config.build_preflight_response(origin)
    allowed = config.is_origin_allowed(origin)
    print(f"{origin}: allowed={allowed}, preflight={status}")

Dynamic Origin Resolution

Resolve allowed origins dynamically from a database or configuration service.

from typing import Dict, List, Optional, Set
import time

class DynamicOriginResolver:
    def __init__(self, cache_ttl: int = 60):
        self.cache: Dict[str, Set[str]] = {}
        self.cache_timestamp: float = 0
        self.cache_ttl = cache_ttl

    def refresh_origins(self, app_origins: Dict[str, List[str]]):
        self.cache = {
            app: set(origins)
            for app, origins in app_origins.items()
        }
        self.cache_timestamp = time.time()

    def get_allowed_origins(self, app: str) -> Set[str]:
        if time.time() - self.cache_timestamp > self.cache_ttl:
            self.cache.pop(app, None)
        return self.cache.get(app, set())

    def is_origin_allowed(self, app: str,
                          origin: str) -> bool:
        allowed = self.get_allowed_origins(app)
        return origin in allowed

    def add_origin(self, app: str, origin: str):
        if app not in self.cache:
            self.cache[app] = set()
        self.cache[app].add(origin)

resolver = DynamicOriginResolver()
resolver.refresh_origins({
    "dashboard": ["https://dashboard.dodatech.com"],
    "api": ["https://app.durga-antivirus.com"]
})
print(resolver.is_origin_allowed(
    "api", "https://app.durga-antivirus.com"
))
print(resolver.is_origin_allowed(
    "api", "https://evil.com"
))

Per-Route CORS Policies

Different API routes may require different CORS policies.

from typing import Dict, Optional, Tuple
import re

class PerRouteCORS:
    def __init__(self):
        self.default_config = CORSConfig()
        self.route_configs: Dict[str, CORSConfig] = {}

    def set_route_config(self, path_pattern: str,
                         config: CORSConfig):
        self.route_configs[path_pattern] = config

    def get_config(self, path: str) -> CORSConfig:
        for pattern, config in self.route_configs.items():
            if re.search(pattern, path):
                return config
        return self.default_config

    def handle_preflight(self, path: str, origin: str
                         ) -> Tuple[int, Dict]:
        config = self.get_config(path)
        return config.build_preflight_response(origin)

    def handle_cors(self, path: str, origin: str
                    ) -> Dict:
        config = self.get_config(path)
        return config.build_cors_headers(origin)

cors = PerRouteCORS()
public_config = CORSConfig()
public_config.add_origin("*")
cors.set_route_config(r"^/api/public", public_config)

private_config = CORSConfig()
private_config.add_origin("https://app.example.com")
cors.set_route_config(r"^/api/private", private_config)

for path in ["/api/public/health", "/api/private/users"]:
    status, headers = cors.handle_preflight(
        path, "https://app.example.com"
    )
    print(f"{path}: preflight status={status}, "
          f"origin={headers.get('Access-Control-Allow-Origin')}")

Common Mistakes

Mistake 1: Using Access-Control-Allow-Origin: *

Using a wildcard origin disables credentials (cookies, auth headers) and is unsafe for authenticated APIs.

Mistake 2: Reflecting Origin Without Validation

An attacker can set any Origin header. Always validate against an allowlist.

Mistake 3: Not Handling Preflight Properly

Preflight OPTIONS requests must respond with correct CORS headers. A missing preflight handler breaks browser requests.

Mistake 4: Misconfigured Exposed Headers

Custom response headers are not accessible to JavaScript unless listed in Access-Control-Expose-Headers.

Mistake 5: Forgetting Vary: Origin

Without Vary: Origin, CDNs and caches may serve CORS responses intended for one origin to another origin.

Practice Questions

  1. What is a CORS preflight request and when is it triggered?
  2. Why does Access-Control-Allow-Origin: * not work with credentials?
  3. What is the purpose of the Vary: Origin header?
  4. How does CORS configuration differ for public vs private APIs?
  5. What happens if the gateway does not handle OPTIONS requests?

Challenge

Build a CORS middleware for the gateway that supports per-route allowed origins, handles preflight requests, correctly sets Vary: Origin, supports credentials, and validates the Origin header against a configurable allowlist.

FAQ

What is CORS and why do APIs need it?

CORS is a browser security mechanism that controls which web origins can access API resources. Without it, browsers block cross-origin requests by default.

Should the gateway or the backend handle CORS?

The gateway should handle CORS centrally. This keeps CORS configuration in one place and prevents inconsistencies across services.

What is the difference between simple and preflight requests?

Simple requests (GET, HEAD, POST with standard content types) do not trigger preflight. All other requests send an OPTIONS preflight first.

How do you handle CORS with WebSocket connections?

WebSocket connections are not subject to CORS. However, the browser checks the Origin header during the handshake. Validate it at the gateway.

Can CORS be configured per environment?

Yes. In development, allow multiple origins including localhost. In production, restrict to known domains only.

Mini Project

Build a CORS middleware for the gateway that supports per-route origin allowlists with wildcard subdomain matching, automatic preflight handling, configurable allowed methods and headers, credentials support, and Vary: Origin header injection.

What's Next

Learn about Header Manipulation for request and response header transformation, or explore Gateway Security for comprehensive protection strategies.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro