CORS Configuration - Comprehensive Guide
Overview
This document describes the permanent, environment-aware CORS configuration that eliminates CORS issues across all deployment environments (local, Docker, Azure Container Groups, Application Gateway).
Problem Statement
CORS (Cross-Origin Resource Sharing) failures occur when:
1. Browser loads UI from https://example.com
2. UI makes API call to https://example.com/api/*
3. API checks Origin header against allowed origins
4. Origin not in allow-list → CORS rejection ❌
Traditional approaches hardcode origins (e.g., localhost:8080), which breaks in:
- Azure deployments (public FQDN different)
- Different environments (dev/test/prod URLs differ)
- Dynamic infrastructure (IPs change on redeployment)
Solution: Intelligent Multi-Environment CORS
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ CORS Configuration Flow │
└─────────────────────────────────────────────────────────────────┘
1. DEPLOYMENT SCRIPT (deploy-apps.sh)
├─ Detects Application Gateway FQDN from Key Vault
├─ Builds environment-specific CORS origins list
└─ Passes to Bicep: --parameters corsOrigins="https://..."
2. BICEP TEMPLATE (apps.bicep)
├─ Sets APP_CORS_ORIGINS environment variable (explicit origins)
├─ Sets APP_GATEWAY_URL environment variable (for auto-detection)
└─ Deploys to ACI Container Group
3. API CONFIG (config.py)
├─ Loads APP_CORS_ORIGINS (if set) → explicit configuration
├─ OR detects APP_GATEWAY_URL → Azure deployment mode
├─ OR detects DOCKER_CONTAINER → Docker Compose mode
└─ OR defaults to localhost development ports
4. FASTAPI MIDDLEWARE (app.py)
├─ CORSMiddleware validates Origin header
├─ Logs CORS preflight requests (OPTIONS)
└─ Warns on rejected origins (debug mode)
Key Features
1. Intelligent Auto-Detection
# API config.py - Smart defaults based on environment
if APP_GATEWAY_URL:
# Azure: Use Application Gateway public FQDN
origins = ["https://gateway.cloudapp.azure.com"]
elif DOCKER_CONTAINER:
# Docker: Include container networking + localhost
origins = ["http://ui:8080", "http://localhost:8080"]
else:
# Local: Common dev ports (Vite, React, etc.)
origins = ["http://localhost:5173", "http://localhost:3000"]
2. Deployment Script Integration
# deploy-apps.sh - Automatic Application Gateway detection
APP_GATEWAY_FQDN=$(az keyvault secret show \
--vault-name "$KV_NAME" \
--name "substrate-appGatewayPublicFqdn" \
--query value -o tsv)
CORS_ORIGINS="https://${APP_GATEWAY_FQDN},http://localhost:8080"
3. CORS Debugging Middleware
# app.py - Automatic logging of CORS issues
@app.middleware("http")
async def cors_debug_middleware(request: Request, call_next):
if request.method == "OPTIONS":
logger.info(f"CORS Preflight: {origin} → {allowed}")
if not origin_allowed:
logger.warning(f"CORS Rejected: {origin}")
Configuration by Environment
Local Development
Auto-Configured Origins:
http://localhost:3000 # Create React App default
http://localhost:5173 # Vite default
http://localhost:5174 # Vite alternative port
http://localhost:8080 # Docker Compose UI
http://127.0.0.1:* # IP variants
Manual Override:
Docker Compose
Auto-Configured Origins:
http://ui:8080 # Container-to-container
http://loan-defenders-ui:8080 # Docker service name
http://localhost:8080 # Host browser access
http://localhost:5173 # Dev server (if mounted)
Configuration:
# docker-compose.yml
api:
environment:
DOCKER_CONTAINER: "true" # Enables Docker auto-detection
# APP_CORS_ORIGINS: "..." # Optional explicit override
Azure Container Groups (Private)
Auto-Configured Origins:
http://localhost:8080 # Bastion Jump Box browser access
http://10.0.x.x:8080 # Private IP (if needed)
Configuration:
# deploy-apps.sh passes localhost origins for Bastion access
CORS_ORIGINS="http://localhost:8080,http://localhost:5173"
Azure with Application Gateway (Public)
Auto-Configured Origins:
https://ldfdev8.eastus.cloudapp.azure.com # Application Gateway FQDN
http://ldfdev8.eastus.cloudapp.azure.com # HTTP variant (redirects to HTTPS)
http://localhost:8080 # Dev/testing via Bastion
Configuration:
# deploy-apps.sh automatically detects Application Gateway
APP_GATEWAY_FQDN=$(retrieve_output_robust "appGatewayPublicFqdn" ...)
CORS_ORIGINS="https://${APP_GATEWAY_FQDN},http://${APP_GATEWAY_FQDN}"
Production with Custom Domain
Required Configuration:
# Set before deployment
export CUSTOM_DOMAIN="yourdomain.com"
# deploy-apps.sh adds:
CORS_ORIGINS="https://yourdomain.com,https://www.yourdomain.com"
DNS Configuration:
yourdomain.com → CNAME → ldfprod.eastus.cloudapp.azure.com
www.yourdomain.com → CNAME → ldfprod.eastus.cloudapp.azure.com
Troubleshooting
Issue 1: CORS Rejection in Browser
Symptoms:
Access to fetch at 'https://api.example.com/api/chat' from origin
'https://ui.example.com' has been blocked by CORS policy
Diagnosis:
# Check API logs for CORS warnings
az container logs --name <container-group> --resource-group <rg> \
--container-name api | grep "CORS"
# Look for:
[WARN] CORS Origin Not Allowed: https://ui.example.com
Fix: 1. Add origin to deployment script:
- Redeploy:
Issue 2: Preflight Failures
Symptoms:
Diagnosis:
# Test preflight manually
curl -X OPTIONS https://api.example.com/api/chat \
-H "Origin: https://ui.example.com" \
-H "Access-Control-Request-Method: POST" \
-v
Fix: - Check nginx.conf doesn't block OPTIONS requests - Verify CORSMiddleware is registered (should be automatic)
Issue 3: Wrong Origins Configured
Symptoms: API accepts requests from wrong origins or rejects valid ones.
Diagnosis:
# Check runtime configuration
az container exec --name <container-group> --resource-group <rg> \
--container-name api --exec-command "printenv APP_CORS_ORIGINS"
Fix: Explicitly set CORS_ORIGINS in deployment:
Testing CORS Configuration
Test 1: Preflight Request
curl -X OPTIONS http://localhost:8000/loan-defenders/api/chat \
-H "Origin: http://localhost:8080" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type" \
-v
# Expected response headers:
# Access-Control-Allow-Origin: http://localhost:8080
# Access-Control-Allow-Methods: POST, GET, OPTIONS, ...
# Access-Control-Allow-Headers: Content-Type
Test 2: Actual Request
curl -X POST http://localhost:8000/loan-defenders/api/chat \
-H "Origin: http://localhost:8080" \
-H "Content-Type: application/json" \
-d '{"user_message": "test", "session_id": "test"}' \
-v
# Expected response header:
# Access-Control-Allow-Origin: http://localhost:8080
Test 3: Browser Console
// In browser console (while on UI page)
fetch('http://localhost:8000/loan-defenders/api/health', {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
})
.then(r => r.json())
.then(console.log)
.catch(console.error);
// Should NOT see CORS errors
Security Considerations
Development vs Production
Development (Permissive):
# Multiple localhost ports for flexibility
origins = [
"http://localhost:3000",
"http://localhost:5173",
"http://localhost:8080",
]
Production (Restrictive):
Never Use Wildcards in Production
❌ INSECURE:
✅ SECURE:
Credentials and Cookies
# CORSMiddleware configuration
allow_credentials=True # Required for cookies/auth tokens
allow_origins=["https://yourdomain.com"] # MUST be explicit (not "*")
Maintenance
Adding New Origins
For Permanent Origins:
1. Edit deploy-apps.sh:
- Redeploy Apps layer
For Temporary Testing:
# Override at deployment time
export APP_CORS_ORIGINS="https://test-origin.com,$APP_CORS_ORIGINS"
./infrastructure/scripts/deploy-apps.sh dev <rg>
Updating for New Environments
# 1. Add environment-specific origins
if [ "$ENV" == "staging" ]; then
CORS_ORIGINS_ARRAY+=("https://staging.yourdomain.com")
fi
# 2. Deploy
./infrastructure/scripts/deploy-apps.sh staging <rg>
References
Last Updated: 2025-10-29
Status: Active
Maintainer: Infrastructure Team