CORS policy configuration for .NET APIs — named policies, origin/method/header control, credentials handling, per-endpoint CORS, preflight caching, and common mistakes. Trigger: CORS, cross-origin, AddCors, AllowOrigins, preflight, Access-Control.
From dotnet-ai-kitnpx claudepluginhub faysilalshareef/dotnet-ai-kit --plugin dotnet-ai-kitThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
AllowAnyOrigin() disables the browser's same-origin protectionAccess-Control-Allow-Origin: * when Access-Control-Allow-Credentials: true — browsers will block the responseUseCors() must appear after UseRouting() and before UseAuthentication() / UseAuthorization():5173, API on :5000)// Program.cs
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy
.WithOrigins("https://app.example.com")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Content-Type", "Authorization");
});
});
var app = builder.Build();
app.UseRouting();
app.UseCors(); // after UseRouting, before UseAuth
app.UseAuthentication();
app.UseAuthorization();
Use named policies when different endpoint groups need different CORS rules.
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowSPA", policy =>
policy
.WithOrigins(
"https://app.example.com",
"https://staging.example.com")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders(
"Content-Type",
"Authorization",
"X-Request-Id"));
options.AddPolicy("AllowReporting", policy =>
policy
.WithOrigins("https://reports.example.com")
.WithMethods("GET")
.WithHeaders("Authorization"));
options.AddPolicy("AllowPublic", policy =>
policy
.AllowAnyOrigin()
.WithMethods("GET")
.WithHeaders("Content-Type"));
});
When your SPA sends cookies or Authorization headers with credentials: "include", you must explicitly allow credentials. This is incompatible with AllowAnyOrigin().
options.AddPolicy("AllowSPAWithCredentials", policy =>
policy
.WithOrigins("https://app.example.com")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Content-Type", "Authorization")
.AllowCredentials());
If you need to support multiple subdomains dynamically while still using credentials, use SetIsOriginAllowed:
options.AddPolicy("AllowSubdomains", policy =>
policy
.SetIsOriginAllowed(origin =>
{
var host = new Uri(origin).Host;
return host == "example.com"
|| host.EndsWith(".example.com",
StringComparison.OrdinalIgnoreCase);
})
.AllowCredentials()
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Content-Type", "Authorization"));
Apply named policies to specific endpoint groups instead of globally.
// Minimal API — RequireCors on a group
var api = app.MapGroup("/api")
.RequireCors("AllowSPA");
api.MapGet("/orders", GetOrders);
api.MapPost("/orders", CreateOrder);
// Public health check — different policy
app.MapGet("/health", () => Results.Ok("Healthy"))
.RequireCors("AllowPublic");
With controllers, use the [EnableCors] attribute:
[ApiController]
[Route("api/[controller]")]
[EnableCors("AllowSPA")]
public class OrdersController : ControllerBase
{
// All actions inherit "AllowSPA"
[DisableCors]
[HttpGet("internal-status")]
public IActionResult InternalStatus() => Ok();
}
Browsers send an OPTIONS preflight request before non-simple cross-origin requests. Cache the preflight response to avoid redundant round-trips.
options.AddPolicy("AllowSPA", policy =>
policy
.WithOrigins("https://app.example.com")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Content-Type", "Authorization")
.SetPreflightMaxAge(TimeSpan.FromHours(1)));
Use environment-specific CORS configuration so development is convenient while production stays locked down.
if (builder.Environment.IsDevelopment())
{
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
policy
.WithOrigins(
"https://localhost:5173",
"http://localhost:5173",
"https://localhost:3000")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
}
else
{
var allowedOrigins = builder.Configuration
.GetSection("Cors:AllowedOrigins")
.Get<string[]>() ?? [];
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
policy
.WithOrigins(allowedOrigins)
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders(
"Content-Type",
"Authorization",
"X-Request-Id")
.AllowCredentials()
.SetPreflightMaxAge(
TimeSpan.FromHours(1)));
});
}
// appsettings.Production.json
{
"Cors": {
"AllowedOrigins": [
"https://app.example.com",
"https://admin.example.com"
]
}
}
For complex projects, bind CORS configuration to a strongly-typed options class.
public sealed class CorsOptions
{
public const string SectionName = "Cors";
public string[] AllowedOrigins { get; init; } = [];
public string[] AllowedMethods { get; init; } =
["GET", "POST", "PUT", "DELETE"];
public string[] AllowedHeaders { get; init; } =
["Content-Type", "Authorization"];
public bool AllowCredentials { get; init; } = true;
public int PreflightMaxAgeSeconds { get; init; } = 3600;
}
// Registration
var corsConfig = builder.Configuration
.GetSection(CorsOptions.SectionName)
.Get<CorsOptions>() ?? new CorsOptions();
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy
.WithOrigins(corsConfig.AllowedOrigins)
.WithMethods(corsConfig.AllowedMethods)
.WithHeaders(corsConfig.AllowedHeaders)
.SetPreflightMaxAge(
TimeSpan.FromSeconds(
corsConfig.PreflightMaxAgeSeconds));
if (corsConfig.AllowCredentials)
policy.AllowCredentials();
});
});
| Mistake | Problem | Fix |
|---|---|---|
AllowAnyOrigin() + AllowCredentials() | CORS spec violation — browsers block the response | Use WithOrigins(...) with explicit origins when credentials are needed |
UseCors() after UseAuthorization() | CORS middleware never runs for unauthorized preflight requests, returning 401 | Place UseCors() before UseAuthentication() and UseAuthorization() |
| Origins with trailing slash | https://app.example.com/ does not match https://app.example.com | Remove trailing slashes from all origin strings |
Hardcoded localhost origins in production | Opens your API to any local dev machine | Use environment-conditional configuration (see Development vs Production) |
Missing OPTIONS method in WithMethods() | Not needed — the CORS middleware handles preflight OPTIONS automatically | Do not add OPTIONS to WithMethods(); it is implicit |
AllowAnyOrigin() on authenticated endpoints | Any website can make authenticated requests on behalf of your users | Restrict to known origins |
Wildcard headers with AllowAnyHeader() | Exposes your API to unexpected custom headers | List only the headers your clients actually send |
| Not exposing response headers | Client JavaScript cannot read custom response headers unless exposed | Use WithExposedHeaders("X-Pagination", "X-Request-Id") |
| Duplicating CORS headers in both middleware and reverse proxy | Browsers reject responses with duplicate Access-Control-Allow-Origin | Configure CORS in exactly one layer |
| Scenario | Policy Configuration |
|---|---|
| Single SPA, same domain, different port (dev) | Default policy, WithOrigins("https://localhost:PORT"), AllowCredentials() |
| Single SPA, different domain (prod) | Default policy, WithOrigins("https://app.example.com"), AllowCredentials() |
| Multiple SPAs, different domains | Named policies per SPA group, RequireCors("PolicyName") per endpoint group |
| Public read-only API | AllowAnyOrigin(), WithMethods("GET"), no credentials |
| Subdomain wildcard | SetIsOriginAllowed() with domain suffix check, AllowCredentials() |
| Server-to-server only | No CORS configuration needed |
| API gateway handles CORS | No CORS in .NET — configure at the gateway layer only |
| Mixed public + authenticated endpoints | Named policies: one restrictive with credentials, one permissive without |
| Anti-Pattern | Why It Is Harmful | Better Approach |
|---|---|---|
AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader() | Effectively disables CORS protection entirely | Specify exact origins, methods, and headers |
SetIsOriginAllowed(_ => true) | Equivalent to AllowAnyOrigin() but bypasses the wildcard-credentials check — worst of both worlds | Validate the origin against an allowlist or domain pattern |
| Copying CORS config from Stack Overflow without reviewing | Most SO answers use AllowAny* for simplicity — not production-safe | Follow least-privilege principle for each policy |
| One global permissive policy for all endpoints | Public endpoints and authenticated endpoints have different threat models | Use named policies and apply per-group |
| Relying on CORS as an authentication mechanism | CORS is browser-enforced only — curl, Postman, and servers ignore it | Always enforce server-side authentication and authorization |
| Adding CORS headers manually via middleware | Bypasses the built-in CORS negotiation logic and is error-prone | Use AddCors() and UseCors() exclusively |
AddCors in Program.cs or Startup.csUseCors in the middleware pipeline[EnableCors] or [DisableCors] attributes on controllersRequireCors on minimal API endpoint groupsAccess-Control-Allow-Origin header manipulationAddCors() with named policies matching your endpoint groupsUseCors() in the correct middleware position (after routing, before auth)RequireCors() or [EnableCors()]SetPreflightMaxAge to reduce OPTIONS trafficAccess-Control-* response headers on preflight and actual requests