Skip to content

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:

# .env file or shell
export APP_CORS_ORIGINS="http://localhost:5173,http://localhost:3000"

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:

# deploy-apps.sh
CORS_ORIGINS="${CORS_ORIGINS},https://ui.example.com"

  1. Redeploy:
    ./infrastructure/scripts/deploy-apps.sh dev <rg>
    

Issue 2: Preflight Failures

Symptoms:

Preflight response has HTTP status 405 (Method Not Allowed)

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:

./infrastructure/scripts/deploy-apps.sh dev <rg> \
    --cors-override "https://correct-origin.com"

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):

# Only known public domains
origins = [
    "https://yourdomain.com",
    "https://www.yourdomain.com",
]

Never Use Wildcards in Production

❌ INSECURE:

cors_origins = ["*"]  # Allows ANY origin - CSRF risk

✅ SECURE:

cors_origins = [
    "https://yourdomain.com",
    "https://app.yourdomain.com",
]

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:

CORS_ORIGINS_ARRAY+=("https://new-domain.com")

  1. 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