Cross-Origin
Resource
Sharing (CORS) is one of the most commonly misconfigured
features in
modern
APIs. Many
developers
enable AllowAnyOrigin() during
development and accidentally ship it to
production —
creating unnecessary security risks.
In this article, you'll learn how to implement CORS properly in ASP.NET Core for real-world production environments.
We’ll cover:
- What CORS actually is
- Why browsers block requests
- Secure production configuration
- Environment-based policies
- Handling credentials correctly
- Common mistakes
- Best practices for enterprise APIs
Official documentation:
ASP.NET Core CORS Documentation
What is CORS?
CORS (Cross-Origin Resource Sharing) is a browser security mechanism that controls how resources are requested from different domains.
Example:
https://app.example.com
Backend API:
https://api.example.com
Since the origins are different, the browser enforces CORS rules.
Without proper configuration, requests fail with errors like:
Access to fetch at 'https://api.example.com'
from origin 'https://app.example.com'
has been blocked by CORS policy
Understanding Origin
An origin consists of:
Scheme + Host + Port
Examples:
| URL | Origin |
|---|---|
| https://example.com | Different |
| http://example.com | Different |
| https://api.example.com | Different |
| https://example.com:5001 | Different |
Even a port change creates a different origin.
Why CORS Exists
CORS protects users from malicious websites making unauthorized requests using browser credentials.
Without CORS, a malicious site could:
- Read sensitive API responses
- Steal authenticated data
- Abuse user sessions
CORS is enforced by browsers, not by servers.
The Biggest Production Mistake
Many tutorials show this:
builder.Services.AddCors(options =>
{
options.AddPolicy("OpenPolicy", policy =>
{
policy
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
- This is acceptable only for local development.
- Never use unrestricted CORS in production for authenticated APIs.
Production-Ready CORS Configuration
The correct approach is:
- Explicitly allow trusted origins
- Restrict methods
- Restrict headers
- Handle credentials securely
- Separate environments
Step 1 — Configure Allowed Origins
appsettings.json
{
"Cors": {
"AllowedOrigins": [
"https://app.example.com",
"https://admin.example.com"
]
}
}
This keeps configuration centralized and environment-friendly.
Step 2 — Register CORS Policy
Program.cs (.NET 6+)
// Create builder
var builder = WebApplication.CreateBuilder(args);
// Read allowed origins from configuration
var allowedOrigins = builder.Configuration
.GetSection("Cors:AllowedOrigins")
.Get<string[]>();
// Register CORS
builder.Services.AddCors(options =>
{
options.AddPolicy("ProductionCorsPolicy", policy =>
{
policy
// Allow only trusted domains
.WithOrigins(allowedOrigins)
// Restrict HTTP methods
.WithMethods("GET", "POST", "PUT", "DELETE")
// Restrict headers
.WithHeaders("Content-Type", "Authorization")
// Allow cookies/auth tokens if needed
.AllowCredentials();
});
});
// Add controllers
builder.Services.AddControllers();
var app = builder.Build();
// Use CORS before authentication/authorization
app.UseCors("ProductionCorsPolicy");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Why AllowCredentials() Matters
If your frontend sends:
- Cookies
- JWT bearer tokens
- Session authentication
- Then credentials handling becomes important.
Example frontend request:
fetch("https://api.example.com/data", {
credentials: "include"
});
When credentials are enabled:
AllowAnyOrigin()is forbidden- Origins must be explicit
- This is intentional for security reasons.
Correct Middleware Order
Middleware order matters in ASP.NET Core.
Correct:
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
Incorrect ordering can cause:
- Failed preflight requests
- Missing headers
- Authentication issues
Environment-Based CORS
Production and development usually require different rules.
appsettings.Development.json
{
"Cors": {
"AllowedOrigins": [
"http://localhost:3000",
"https://localhost:5173"
]
}
}
appsettings.Production.json
{
"Cors": {
"AllowedOrigins": [
"https://app.example.com"
]
}
}
This prevents accidentally exposing development origins in production.
Supporting Multiple Frontends
Production APIs often support:
- Web applications
- Admin dashboards
- Mobile gateways
- Partner portals
Use explicit origin lists:
policy.WithOrigins(
"https://app.example.com",
"https://admin.example.com",
"https://partner.example.com"
);
Restrict Headers Properly
Avoid:
.AllowAnyHeader()
Prefer:
.WithHeaders(
"Content-Type",
"Authorization",
"X-Requested-With"
)
This reduces attack surface and keeps policies predictable.
Restrict Methods Properly
Avoid:
.AllowAnyMethod()
Prefer:
.WithMethods("GET", "POST")
Only expose what your API truly supports.
Handling Preflight Requests
Browsers
send an OPTIONS request before certain cross-origin calls.
This is called a preflight request.
Example:
OPTIONS /api/users HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
ASP.NET Core handles this automatically when CORS is configured correctly.
Common Production Mistakes
1. Using AllowAnyOrigin()
Unsafe for authenticated APIs.
2. Mixing Credentials with Wildcards
Invalid:
.AllowAnyOrigin()
.AllowCredentials()
ASP.NET Core will reject this configuration.
3. Applying CORS Too Late
Wrong middleware order causes failures.
4. Forgetting Reverse Proxy Headers
When using:
- Nginx
- IIS
- Azure App Gateway
- Cloudflare
Ensure proxy configuration does not strip CORS headers.
Applying CORS to Specific Controllers
Instead of globally enabling CORS:
[EnableCors("ProductionCorsPolicy")]
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
}
Namespace required:
using Microsoft.AspNetCore.Cors;
This is useful for:
- Public APIs
- Multi-tenant systems
- Admin-only endpoints
Dynamic CORS for Multi-Tenant SaaS
Some SaaS applications need dynamic origins.
Example:
policy.SetIsOriginAllowed(origin =>
{
return origin.EndsWith(".example.com");
});
Use carefully.
Validate origins properly to avoid subdomain abuse.
Production Security Recommendations
Use HTTPS Only
Always allow secure origins:
https://app.example.com
Never allow plain HTTP in production.
Log Rejected Origins
Track suspicious cross-origin attempts.
Example use cases:
- Threat monitoring
- Misconfigured clients
- Abuse detection
Keep Origins Configurable
Avoid hardcoding origins directly into source code.
Use:
- Configuration files
- Environment variables
- Secret managers
Separate Public and Internal APIs
Internal admin APIs should not share permissive CORS rules with public APIs.
Example Enterprise Setup
builder.Services.AddCors(options =>
{
options.AddPolicy("EnterprisePolicy", policy =>
{
policy
.WithOrigins(
"https://app.example.com",
"https://admin.example.com"
)
.WithMethods("GET", "POST", "PUT")
.WithHeaders(
"Authorization",
"Content-Type"
)
.AllowCredentials();
});
});
This configuration is:
- Explicit
- Secure
- Maintainable
- Production-friendly
Testing CORS
Useful tools:
- Browser DevTools
- Postman
- curl
- Network tab inspection
Example curl request:
curl -H "Origin: https://app.example.com" \
--verbose \
https://api.example.com/users
Check response headers:
Access-Control-Allow-Origin: https://app.example.com
Final Thoughts
CORS is not just a frontend issue — it is a critical API security layer.
A production-ready CORS strategy should:
- Explicitly trust origins
- Avoid wildcards
- Restrict methods and headers
- Use environment-specific configuration
- Support secure credential handling
- Follow correct middleware order
If configured properly, CORS becomes predictable, secure, and easy to maintain even in large enterprise systems.
Further reading: