CORS at the API Gateway — Cross-Origin Resource Sharing Configuration
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
- What is a CORS preflight request and when is it triggered?
- Why does Access-Control-Allow-Origin: * not work with credentials?
- What is the purpose of the Vary: Origin header?
- How does CORS configuration differ for public vs private APIs?
- 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
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