Cors Configuration Feature Documentation¶
Configure browser cross-origin access through fluent, settings-driven CORS policies.
Overview¶
CORS (Cross-Origin Resource Sharing) is a security mechanism that controls which origins (domains) can access your API from a browser. By default, browsers block cross-origin requests to protect users from malicious websites.
Builds on the standard ASP.NET Cors implementation and basicaly provides a configuration (appsettings) system for it.
This feature uses a flexible, configuration-driven CORS setup that supports:
- Multiple named policies for different scenarios
- Environment-specific configuration (Development vs Production)
- Global default policies or per-endpoint control
- Wildcard subdomain matching
- Fine-grained control over origins, methods, headers, and credentials
Key Concepts¶
- Origin: The combination of scheme, host, and port (e.g.,
https://example.com:443) - Preflight Request: An OPTIONS request the browser sends before certain cross-origin requests
- Simple Request: GET/POST requests that don't trigger preflight
- Credentials: Cookies, authorization headers, or client certificates
Configuration Schema¶
CorsConfiguration Properties¶
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
Enabled |
bool |
Yes | false |
Whether CORS is enabled. When false, all cross-origin requests are blocked. |
DefaultPolicy |
string |
No | null |
Name of the policy to apply globally. Leave null for endpoint-level control only. |
Policies |
Dictionary<string, CorsPolicyOptions> |
Yes* | {} |
Named policies. At least one required when Enabled is true. |
*Required when Enabled is true
CorsPolicyOptions Properties¶
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
AllowedOrigins |
string[] |
No | null |
Array of allowed origins (e.g., ["https://example.com"]). Cannot use with AllowAnyOrigin. |
AllowedMethods |
string[] |
No | null |
Array of allowed HTTP methods (e.g., ["GET", "POST"]). Cannot use with AllowAnyMethod. |
AllowedHeaders |
string[] |
No | null |
Array of allowed request headers. Cannot use with AllowAnyHeader. |
ExposeHeaders |
string[] |
No | null |
Array of response headers to expose to JavaScript. |
AllowCredentials |
bool? |
No | false |
Allow credentials (cookies, auth headers). Cannot use with AllowAnyOrigin = true. |
AllowAnyOrigin |
bool? |
No | false |
Allow any origin (). Cannot use with AllowCredentials = true. Use only in development!* |
AllowAnyMethod |
bool? |
No | false |
Allow any HTTP method. Overrides AllowedMethods. |
AllowAnyHeader |
bool? |
No | false |
Allow any header. Overrides AllowedHeaders. |
AllowWildcardSubdomains |
bool? |
No | false |
Enable wildcard subdomain matching (e.g., *.example.com). |
PreflightMaxAgeSeconds |
int? |
No | null |
Preflight cache duration in seconds. Recommended: 600-3600 for production. |
Configuration Examples¶
Example 1: Production (Secure Configuration)¶
Specific origins with credentials support:
{
"Cors": {
"Enabled": true,
"DefaultPolicy": "ProductionPolicy",
"Policies": {
"ProductionPolicy": {
"AllowedOrigins": [
"https://www.example.com",
"https://app.example.com"
],
"AllowAnyMethod": true,
"AllowAnyHeader": true,
"AllowCredentials": true,
"PreflightMaxAgeSeconds": 3600
}
}
}
}
Example 2: Development (Permissive Configuration)¶
Allow common localhost ports (included in appsettings.Development.json):
{
"Cors": {
"Enabled": true,
"DefaultPolicy": "LocalhostPolicy",
"Policies": {
"LocalhostPolicy": {
"AllowedOrigins": [
"https://localhost:5001",
"https://localhost:5000",
"https://localhost:3000",
"https://localhost:3001",
"https://localhost:4200",
"http://localhost:5001",
"http://localhost:5000",
"http://localhost:3000",
"http://localhost:3001"
],
"AllowAnyMethod": true,
"AllowAnyHeader": true,
"AllowCredentials": true
}
}
}
}
Common ports:
- 5001/5000: ASP.NET Core default ports
- 3000/3001: React, Node.js default ports
- 4200: Angular default port
Example 3: Public API (No Credentials)¶
Allow any origin without credentials:
{
"Cors": {
"Enabled": true,
"DefaultPolicy": "PublicApiPolicy",
"Policies": {
"PublicApiPolicy": {
"AllowAnyOrigin": true,
"AllowAnyMethod": true,
"AllowAnyHeader": true,
"PreflightMaxAgeSeconds": 600
}
}
}
}
⚠️ Note: Cannot use AllowCredentials: true with AllowAnyOrigin: true.
Example 4: Wildcard Subdomains¶
Allow any subdomain of example.com:
{
"Cors": {
"Enabled": true,
"DefaultPolicy": "SubdomainPolicy",
"Policies": {
"SubdomainPolicy": {
"AllowedOrigins": [
"https://example.com"
],
"AllowWildcardSubdomains": true,
"AllowAnyMethod": true,
"AllowAnyHeader": true,
"AllowCredentials": true
}
}
}
}
This allows:
https://api.example.comhttps://app.example.comhttps://admin.example.com- Any other subdomain of
example.com
Example 5: Multiple Named Policies¶
Different policies for different endpoints:
{
"Cors": {
"Enabled": true,
"DefaultPolicy": null,
"Policies": {
"FrontendPolicy": {
"AllowedOrigins": ["https://app.example.com"],
"AllowAnyMethod": true,
"AllowAnyHeader": true,
"AllowCredentials": true
},
"PublicApiPolicy": {
"AllowAnyOrigin": true,
"AllowedMethods": ["GET"],
"AllowAnyHeader": true
},
"AdminPolicy": {
"AllowedOrigins": ["https://admin.example.com"],
"AllowedMethods": ["GET", "POST", "PUT", "DELETE"],
"AllowAnyHeader": true,
"AllowCredentials": true
}
}
}
}
Example 6: API-Only (Specific Methods and Headers)¶
Restrictive configuration for internal API:
{
"Cors": {
"Enabled": true,
"DefaultPolicy": "RestrictivePolicy",
"Policies": {
"RestrictivePolicy": {
"AllowedOrigins": ["https://internal.example.com"],
"AllowedMethods": ["GET", "POST"],
"AllowedHeaders": ["Content-Type", "Authorization"],
"ExposeHeaders": ["X-Total-Count", "X-Page-Number"],
"AllowCredentials": true,
"PreflightMaxAgeSeconds": 1800
}
}
}
}
Applying Policies¶
Global Default Policy¶
Apply a policy to all endpoints by specifying DefaultPolicy:
{
"Cors": {
"Enabled": true,
"DefaultPolicy": "DefaultPolicy",
"Policies": {
"DefaultPolicy": {
"AllowAnyOrigin": true,
"AllowAnyMethod": true,
"AllowAnyHeader": true
}
}
}
}
In Program.cs:
builder.Services.AddAppCors(builder.Configuration);
// ...
app.UseAppCors(builder.Configuration); // Applies DefaultPolicy globally
Per-Endpoint Policy (Minimal API)¶
Apply different policies to specific Minimal API endpoints using RequireCors():
// Use default policy (configured in DefaultPolicy setting)
app.MapGet("/api/products", () => Results.Ok(products))
.RequireCors();
// Use specific named policy
app.MapGet("/api/products/frontend", () => Results.Ok(frontendData))
.RequireCors("FrontendPolicy");
// Use different policy for admin endpoint
app.MapPost("/api/products/admin", (Product product) =>
{
// Create product logic
return Results.Created($"/api/products/{product.Id}", product);
})
.RequireCors("AdminPolicy");
// Group multiple endpoints with same policy
var productsGroup = app.MapGroup("/api/products")
.RequireCors("FrontendPolicy");
productsGroup.MapGet("/", () => Results.Ok(products));
productsGroup.MapGet("/{id}", (int id) => Results.Ok(GetProduct(id)));
productsGroup.MapPost("/", (Product product) => Results.Created($"/api/products/{product.Id}", product));
Note: Unlike controller-based APIs, Minimal API does not support [DisableCors] directly. Simply omit RequireCors() on endpoints that should not allow CORS.
Per-Endpoint Policy (Controller-Based)¶
If using controllers, apply policies using the [EnableCors] attribute:
using Microsoft.AspNetCore.Cors;
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
// Use default policy (configured in DefaultPolicy setting)
[EnableCors()]
[HttpGet]
public IActionResult GetPublicProducts()
{
// ...
}
// Use specific named policy
[EnableCors("FrontendPolicy")]
[HttpGet("frontend")]
public IActionResult GetFrontendData()
{
// ...
}
// Use different policy for admin endpoint
[EnableCors("AdminPolicy")]
[HttpPost("admin")]
public IActionResult CreateProduct([FromBody] Product product)
{
// ...
}
// Disable CORS for specific endpoint
[DisableCors]
[HttpGet("internal")]
public IActionResult GetInternalData()
{
// ...
}
}
Policy Precedence¶
When both default and endpoint-level policies are configured:
- Endpoint-level
RequireCors("PolicyName")or[EnableCors("PolicyName")]takes precedence over all - Endpoint-level
RequireCors()or[EnableCors()]without parameters uses the configured default policy - Group-level
MapGroup().RequireCors()applies to all endpoints in the group (Minimal API) - Controller-level
[EnableCors()]applies to all actions (Controller-based) - Global default policy applies when no endpoint configuration is present
[DisableCors]disables CORS for specific controller endpoints
Minimal API Example:
// Global default policy applies to all endpoints
app.UseAppCors(builder.Configuration);
// Group with specific policy
var apiGroup = app.MapGroup("/api")
.RequireCors("ApiPolicy");
apiGroup.MapGet("/products", () => Results.Ok(products)); // Uses ApiPolicy
// Override group policy for specific endpoint
apiGroup.MapGet("/products/special", () => Results.Ok(specialProducts))
.RequireCors("SpecialPolicy"); // Uses SpecialPolicy instead
// Endpoint without RequireCors uses global default
app.MapGet("/public", () => Results.Ok("public data")); // Uses global default policy
Controller-based Example:
[EnableCors()] // Controller-level: uses configured default policy
public class ValuesController : ControllerBase
{
[HttpGet]
public IActionResult Get() { } // Uses default policy from controller
[EnableCors("SpecialPolicy")] // Overrides with specific named policy
[HttpGet("special")]
public IActionResult GetSpecial() { }
[DisableCors] // Disables CORS
[HttpGet("internal")]
public IActionResult GetInternal() { }
}
Security Best Practices¶
Production Recommendations¶
- Never use
AllowAnyOrigin: truewithAllowCredentials: true - This violates the CORS specification
-
The configuration will throw an exception at startup
-
Always specify exact origins in production
"AllowedOrigins": [
"https://www.example.com",
"https://app.example.com"
]
- Avoid
AllowAnyOriginin production - Only use for public APIs without authentication
-
Prefer specific origins or wildcard subdomains
-
Use HTTPS origins
- Always use
https://in production -
HTTP origins (
http://) are only acceptable for localhost in development -
Limit methods to what's needed
"AllowedMethods": ["GET", "POST", "PUT", "DELETE"]
Instead of:
"AllowAnyMethod": true
- Set preflight cache duration
"PreflightMaxAgeSeconds": 3600
Reduces overhead by caching preflight responses
Development vs Production¶
Development (appsettings.Development.json):
- Allow localhost origins with various ports
- Use
AllowCredentials: truefor testing authentication - Shorter or no preflight cache for rapid iteration
Production (appsettings.json):
- Specific production origins only
- Longer preflight cache (3600 seconds)
- Minimal permissions (only required methods/headers)
Wildcard Subdomains¶
Safe when you control all subdomains:
{
"AllowedOrigins": ["https://example.com"],
"AllowWildcardSubdomains": true,
"AllowCredentials": true
}
Unsafe with public subdomains:
- Don't use if anyone can create subdomains (e.g.,
*.github.io)
Troubleshooting¶
Issue: CORS error "No 'Access-Control-Allow-Origin' header"¶
Symptoms:
Access to fetch at 'https://api.example.com/data' from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present.
Solutions:
- Check if CORS is enabled
"Cors": {
"Enabled": true
}
- Verify origin is in AllowedOrigins
- Origin must match exactly (including scheme and port)
-
Don't include trailing slashes:
https://example.com✅ nothttps://example.com/❌ -
Check middleware ordering in
Program.cs
app.UseRouting();
app.UseAppCors(builder.Configuration); // Must be here
app.UseAuthorization();
- Verify configuration is loaded
- Check
appsettings.jsonsyntax is valid - Ensure environment-specific settings are merged correctly
Issue: "CORS policy: Response to preflight request doesn't pass"¶
Symptoms:
Response to preflight request doesn't pass access control check:
The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*'
when the request's credentials mode is 'include'.
Solution:
Cannot use AllowAnyOrigin: true with AllowCredentials: true:
Invalid:
{
"AllowAnyOrigin": true,
"AllowCredentials": true
}
Valid:
{
"AllowedOrigins": ["https://app.example.com"],
"AllowCredentials": true
}
Issue: Preflight OPTIONS request fails¶
Symptoms:
- Browser shows OPTIONS request with 204/200 response
- But actual request (GET/POST) fails
Solutions:
- Ensure methods are allowed
"AllowedMethods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
Or:
"AllowAnyMethod": true
- Check custom headers are allowed
"AllowedHeaders": ["Content-Type", "Authorization", "X-Custom-Header"]
Or:
"AllowAnyHeader": true
- Verify Content-Type is allowed
- For JSON requests, ensure
Content-Type: application/jsonis allowed
Issue: Credentials not being sent¶
Symptoms:
- Cookies or Authorization headers not included in cross-origin requests
Solutions:
- Server-side: Enable AllowCredentials
"AllowCredentials": true
- Client-side: Set credentials mode
Fetch API:
fetch('https://api.example.com/data', {
credentials: 'include' // Send cookies
});
Axios:
axios.get('https://api.example.com/data', {
withCredentials: true
});
jQuery:
$.ajax({
url: 'https://api.example.com/data',
xhrFields: {
withCredentials: true
}
});
Issue: Configuration validation errors on startup¶
Error:
InvalidOperationException: CORS is enabled but no policies are defined.
Solution:
Add at least one policy when Enabled: true:
{
"Cors": {
"Enabled": true,
"Policies": {
"DefaultPolicy": { /* ... */ }
}
}
}
Error:
InvalidOperationException: CORS DefaultPolicy 'MyPolicy' is not defined in Cors:Policies.
Solution:
Ensure DefaultPolicy name matches a policy in Policies:
{
"DefaultPolicy": "MyPolicy",
"Policies": {
"MyPolicy": { /* ... */ }
}
}
Debugging Tips¶
- Check browser console for detailed CORS error messages
- Use browser DevTools Network tab to inspect:
- OPTIONS preflight request and response
- Response headers (
Access-Control-*) - Request headers (
Origin,Access-Control-Request-*) - Test with curl to isolate browser vs server issues:
curl -X OPTIONS https://api.example.com/endpoint \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-i
- Temporarily use permissive settings for debugging:
{
"AllowAnyOrigin": true,
"AllowAnyMethod": true,
"AllowAnyHeader": true
}
Then narrow down to identify the specific restriction causing issues.
Additional Resources¶
Official Documentation¶
Common Scenarios¶
| Scenario | Recommended Configuration |
|---|---|
| Frontend SPA + API | AllowedOrigins with specific domain, AllowCredentials: true |
| Public API | AllowAnyOrigin: true, AllowCredentials: false |
| Multiple subdomains | AllowWildcardSubdomains: true with base domain |
| Development | AllowedOrigins with localhost ports, AllowCredentials: true |
| Microservices | Named policies per service, no default policy |