Skip to content

CORS Configuration

This guide covers configuring Cross-Origin Resource Sharing (CORS) for License Monitor to allow web applications to access the API from different domains.

CORS is a security mechanism that controls which web origins can access your API:

┌─────────────────────────────────────────────────────────────────┐
│ Browser Security Model │
├─────────────────────────────────────────────────────────────────┤
│ │
│ https://dashboard.example.com (Origin A) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Dashboard Application │ │
│ │ │ │
│ │ fetch('https://api.example.com/api/licenses') │ │
│ │ │ │ │
│ └───────────────────────────┼────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Preflight Request │ │
│ │ OPTIONS /api/licenses │ │
│ │ Origin: https://dashboard.example.com │ │
│ └───────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ https://api.example.com (Origin B) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ License Monitor API │ │
│ │ │ │
│ │ CORS Headers: │ │
│ │ Access-Control-Allow-Origin: https://dashboard.example.com│ │
│ │ Access-Control-Allow-Methods: GET, POST │ │
│ │ Access-Control-Allow-Headers: X-API-Key, Content-Type │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘

Configure CORS in config.toml:

[api]
enabled = true
bind_address = "127.0.0.1"
bind_port = 8080
# CORS Configuration
cors_enabled = true
cors_origins = [
"https://dashboard.example.com",
"https://admin.example.com"
]
cors_methods = ["GET", "POST", "OPTIONS"]
cors_headers = ["X-API-Key", "Content-Type", "Authorization"]
cors_max_age = 86400
cors_credentials = true
OptionDescriptionDefault
cors_enabledEnable/disable CORSfalse
cors_originsAllowed origins (exact match)[]
cors_methodsAllowed HTTP methods["GET", "POST", "OPTIONS"]
cors_headersAllowed request headers["Content-Type"]
cors_max_agePreflight cache duration (seconds)86400
cors_credentialsAllow credentials (cookies, auth)false
[api]
cors_enabled = true
cors_origins = ["https://dashboard.example.com"]
cors_credentials = true
[api]
cors_enabled = true
cors_origins = [
"https://dashboard.example.com",
"https://dashboard-staging.example.com",
"https://admin.example.com"
]
[api]
cors_enabled = true
cors_origins = [
"http://localhost:3000",
"http://localhost:5173",
"http://127.0.0.1:3000"
]

For local development only:

[api]
cors_enabled = true
cors_allow_any_origin = true # DEVELOPMENT ONLY

License Monitor sends these CORS headers:

HeaderDescription
Access-Control-Allow-OriginAllowed origin
Access-Control-Allow-MethodsAllowed methods
Access-Control-Allow-HeadersAllowed headers
Access-Control-Max-AgePreflight cache time
Access-Control-Allow-CredentialsAllow credentials
Access-Control-Expose-HeadersExposed headers
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://dashboard.example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: X-API-Key, Content-Type, Authorization
Access-Control-Max-Age: 86400
Access-Control-Allow-Credentials: true

If using a reverse proxy, configure CORS at the proxy level:

server {
location /api/ {
# Handle preflight
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://dashboard.example.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'X-API-Key, Content-Type, Authorization' always;
add_header 'Access-Control-Max-Age' 86400 always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
# Add CORS headers to all responses
add_header 'Access-Control-Allow-Origin' 'https://dashboard.example.com' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Expose-Headers' 'X-Request-ID' always;
proxy_pass http://license_monitor;
}
}
# Map valid origins
map $http_origin $cors_origin {
default "";
"https://dashboard.example.com" $http_origin;
"https://admin.example.com" $http_origin;
}
server {
location /api/ {
if ($cors_origin = "") {
return 403;
}
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
# ... rest of configuration
}
}

Error: “No ‘Access-Control-Allow-Origin’ header”

Access to fetch at 'https://api.example.com/api/health' from origin
'https://dashboard.example.com' has been blocked by CORS policy

Solution: Add the origin to cors_origins:

cors_origins = ["https://dashboard.example.com"]

Error: “Credentials not supported with wildcard origin”

The value of the 'Access-Control-Allow-Origin' header must not be '*'
when the request's credentials mode is 'include'

Solution: Use specific origin instead of wildcard:

cors_origins = ["https://dashboard.example.com"]
cors_credentials = true

Error: “Method not allowed”

Method POST is not allowed by Access-Control-Allow-Methods

Solution: Add the method to allowed methods:

cors_methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
Terminal window
# Test preflight request
curl -X OPTIONS \
-H "Origin: https://dashboard.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: X-API-Key, Content-Type" \
-v https://api.example.com/api/licenses
# Test actual request
curl -X GET \
-H "Origin: https://dashboard.example.com" \
-H "X-API-Key: your-api-key" \
-v https://api.example.com/api/health
// Test CORS from browser console
fetch('https://api.example.com/api/health', {
method: 'GET',
headers: {
'X-API-Key': 'your-api-key',
},
credentials: 'include',
})
.then(response => response.json())
.then(data => console.log('Success:', data))
.catch(error => console.error('CORS Error:', error));
  • Always validate origins against a whitelist
  • Never reflect the Origin header without validation
  • Use exact domain matching, not pattern matching

When cors_credentials = true:

  • Cookies are sent with requests
  • Authorization headers are included
  • Origin cannot be *

Consider stricter CORS for sensitive endpoints:

# Strict CORS for admin endpoints
[api.cors_overrides."/api/admin"]
cors_origins = ["https://admin.example.com"]
cors_methods = ["GET", "POST"]