CORS Configuration Guide¶
Cross-Origin Resource Sharing (CORS) controls which web origins can access your VideoAnnotator API. Proper CORS configuration is critical for security.
Quick Start¶
Default Configuration¶
VideoAnnotator is configured for the official client by default:
# Default: Allows official video-annotation-viewer (19011) + server (18011)
# No configuration needed for standard usage
uv run videoannotator
Console output will show:
[SECURITY] CORS: Allowing official client (port 19011) and server (port 18011)
The default origins are: - Port 19011: video-annotation-viewer (official web client) - Port 18011: VideoAnnotator server (same-origin requests)
For custom clients or testing, use development mode:
uv run videoannotator --dev
# Allows ALL origins (*), disables authentication
Testing CORS¶
# Test official client port (19011) - should succeed
curl -H "Origin: http://localhost:19011" \
-H "Access-Control-Request-Method: POST" \
-X OPTIONS http://localhost:18011/api/v1/jobs
# Response includes:
# access-control-allow-origin: http://localhost:19011
# access-control-allow-methods: POST, GET, OPTIONS, ...
# Test server port (18011) - should succeed
curl -H "Origin: http://localhost:18011" \
-H "Access-Control-Request-Method: POST" \
-X OPTIONS http://localhost:18011/api/v1/jobs
# Disallowed origin - should be rejected
curl -H "Origin: http://unauthorized-site.com" \
-H "Access-Control-Request-Method: POST" \
-X OPTIONS http://localhost:18011/api/v1/jobs
# Response will NOT include access-control-allow-origin header
# Browser will block the request
Configuration¶
Single Custom Origin¶
Set the CORS_ORIGINS environment variable:
# Allow your web app's origin
export CORS_ORIGINS="https://app.yourdomain.com"
uv run videoannotator
Multiple Origins¶
Provide a comma-separated list:
# Allow multiple web apps
export CORS_ORIGINS="https://app.yourdomain.com,https://admin.yourdomain.com,https://staging.yourdomain.com"
uv run videoannotator
Whitespace is automatically stripped, so this also works:
export CORS_ORIGINS="https://app.yourdomain.com, https://admin.yourdomain.com, https://staging.yourdomain.com"
Docker Deployment¶
# docker-compose.yml
services:
videoannotator:
image: videoannotator:latest
environment:
- CORS_ORIGINS=https://app.example.com,https://admin.example.com
ports:
- "18011:18011"
Kubernetes Deployment¶
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: videoannotator
spec:
template:
spec:
containers:
- name: videoannotator
image: videoannotator:latest
env:
- name: CORS_ORIGINS
value: "https://app.example.com,https://admin.example.com"
Security Best Practices¶
⚠️ Never Use Wildcards in Production¶
WRONG (Insecure):
# DO NOT DO THIS
export CORS_ORIGINS="*"
This allows ANY website to access your API, enabling: - Cross-site scripting (XSS) attacks - Data theft - Unauthorized API access
Use Specific Origins¶
CORRECT (Secure):
export CORS_ORIGINS="https://app.yourdomain.com"
Use HTTPS Origins¶
Always use https:// in production:
WRONG:
export CORS_ORIGINS="http://app.example.com" # Insecure
CORRECT:
export CORS_ORIGINS="https://app.example.com" # Secure
Separate Development and Production¶
# Development (.env.development)
CORS_ORIGINS=http://localhost:3000,http://localhost:3001
# Production (.env.production)
CORS_ORIGINS=https://app.yourdomain.com,https://admin.yourdomain.com
Common Scenarios¶
React / Vue / Angular App¶
Local Development:
# Default port for Create React App, Vue CLI, Angular CLI
CORS_ORIGINS=http://localhost:3000
Production:
# Your deployed web app
CORS_ORIGINS=https://app.yourdomain.com
Multiple Environments¶
# Development, staging, and production
CORS_ORIGINS=http://localhost:3000,https://staging-app.yourdomain.com,https://app.yourdomain.com
Mobile App + Web App¶
# Web app only (mobile apps don't need CORS)
CORS_ORIGINS=https://app.yourdomain.com
# Note: Native mobile apps make direct HTTP requests
# and are not subject to CORS restrictions
Internal Lab Network¶
# Lab's internal domains
CORS_ORIGINS=http://research-ui.lab.university.edu,http://admin.lab.university.edu
Microservices Architecture¶
# API gateway and internal services
CORS_ORIGINS=https://gateway.yourdomain.com,https://admin.yourdomain.com
Troubleshooting¶
CORS Error in Browser Console¶
Error Message:
Access to XMLHttpRequest at 'http://localhost:18011/api/v1/jobs'
from origin 'http://localhost:3001' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Solution:
Add your origin to CORS_ORIGINS:
export CORS_ORIGINS="http://localhost:3001"
uv run videoannotator
CORS Works Locally But Not in Production¶
Problem: Local development works, but production fails.
Common Causes: 1. Origin mismatch: Check exact URL (http vs https, www vs non-www)
# If your site is at https://www.example.com, use:
CORS_ORIGINS=https://www.example.com
# NOT:
CORS_ORIGINS=https://example.com # Missing www
-
Environment variable not set: Verify on production server
# SSH into production server echo $CORS_ORIGINS -
Reverse proxy stripping headers: Check nginx/apache config
# nginx - preserve CORS headers proxy_pass_request_headers on;
Preflight Requests Failing¶
Problem: OPTIONS requests return 403/401.
Cause: API requires authentication, but preflight requests don't send credentials.
Solution: VideoAnnotator automatically allows OPTIONS requests. Ensure your client sends:
fetch('http://localhost:18011/api/v1/jobs', {
method: 'GET',
headers: {
'Authorization': 'Bearer va_api_...',
},
credentials: 'include' // Important for CORS with auth
})
CORS Allows Origin But Request Still Fails¶
Problem: Browser shows CORS header, but request blocked.
Possible Causes: 1. Authentication required: Add Authorization header
headers: {
'Authorization': 'Bearer your_api_key'
}
-
Credentials not included: Add credentials flag
fetch(url, { credentials: 'include' }) -
Content-Type issues: Ensure proper Content-Type
headers: { 'Content-Type': 'application/json' }
Advanced Configuration¶
Dynamic CORS Origins¶
For complex scenarios, you can modify src/api/main.py:
import os
# Load from database, config file, etc.
def get_allowed_origins():
# Custom logic to determine origins
if os.environ.get("ENVIRONMENT") == "production":
return ["https://app.prod.com"]
elif os.environ.get("ENVIRONMENT") == "staging":
return ["https://app.staging.com", "https://app.dev.com"]
else:
return ["http://localhost:3000"]
cors_origins = get_allowed_origins()
app.add_middleware(
CORSMiddleware,
allow_origins=cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
CORS with API Gateway¶
If using an API gateway (Kong, AWS API Gateway, etc.), handle CORS there:
Kong:
plugins:
- name: cors
config:
origins:
- https://app.example.com
credentials: true
max_age: 3600
AWS API Gateway:
# serverless.yml
functions:
api:
handler: handler.main
events:
- http:
path: /{proxy+}
method: ANY
cors:
origin: https://app.example.com
headers:
- Authorization
allowCredentials: true
Then disable VideoAnnotator's built-in CORS (API gateway handles it).
Configuration Reference¶
Environment Variables¶
| Variable | Default | Description |
|---|---|---|
CORS_ORIGINS |
http://localhost:3000 |
Comma-separated list of allowed origins |
Allowed Values¶
| Value | Security | Use Case |
|---|---|---|
https://app.example.com |
✅ Secure | Production (recommended) |
http://localhost:3000 |
⚠️ Dev only | Local development |
https://example.com,https://admin.example.com |
✅ Secure | Multiple production origins |
* |
❌ INSECURE | Never use |
CORS Headers¶
VideoAnnotator automatically sets:
Access-Control-Allow-Origin: Matching origin or nullAccess-Control-Allow-Credentials:trueAccess-Control-Allow-Methods:GET, POST, PUT, DELETE, OPTIONS, PATCHAccess-Control-Allow-Headers: All requested headersAccess-Control-Max-Age: Browser-dependent (typically 86400)
Testing¶
Manual Testing¶
# Test from browser console (press F12)
fetch('http://localhost:18011/api/v1/health', {
headers: { 'Authorization': 'Bearer va_api_...' }
})
.then(r => r.json())
.then(console.log)
.catch(console.error)
Automated Testing¶
# tests/integration/test_cors.py
def test_cors_allowed_origin(client):
response = client.options(
"/api/v1/jobs",
headers={
"Origin": "http://localhost:3000",
"Access-Control-Request-Method": "POST"
}
)
assert response.status_code in (200, 204)
assert "access-control-allow-origin" in response.headers
def test_cors_disallowed_origin(client):
response = client.options(
"/api/v1/jobs",
headers={
"Origin": "http://evil.com",
"Access-Control-Request-Method": "POST"
}
)
# Should not include allow-origin header for disallowed origin
if "access-control-allow-origin" in response.headers:
assert response.headers["access-control-allow-origin"] != "http://evil.com"
See Also¶
- Authentication Guide - API key management
- Production Checklist - Complete security guide
- MDN CORS Documentation - Deep dive into CORS