Cross-Origin Resource Sharing (CORS) is a browser security mechanism that controls which origins (domain + scheme + port) can make HTTP requests to your API. By default, browsers block cross-origin requests from JavaScript. CORS allows the server to explicitly permit specific origins, methods, and headers via response headers.

CORS is a browser enforcement mechanism — it does not protect your API from non-browser clients (curl, Postman, server-to-server calls). It only affects browser-initiated requests.

How CORS Works

When a browser makes a cross-origin request, it either sends the request directly (simple requests: GET/POST with standard headers) or first sends a preflight OPTIONS request to check if the server allows the actual request.

Browser → OPTIONS /api/orders (preflight)
          Origin: https://myapp.com
          Access-Control-Request-Method: POST
 
Server  → 200 OK
          Access-Control-Allow-Origin: https://myapp.com
          Access-Control-Allow-Methods: GET, POST
          Access-Control-Allow-Headers: Content-Type, Authorization
 
Browser → POST /api/orders (actual request)

Configuring CORS in ASP.NET Core

// Program.cs — define a named policy
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowFrontend", policy =>
        policy.WithOrigins("https://myapp.com", "https://staging.myapp.com")
              .WithMethods("GET", "POST", "PUT", "DELETE")
              .WithHeaders("Content-Type", "Authorization")
              .AllowCredentials());  // required for cookies/auth headers
 
    // Development: allow any origin (NEVER in production)
    options.AddPolicy("AllowAll", policy =>
        policy.AllowAnyOrigin()
              .AllowAnyMethod()
              .AllowAnyHeader());
});
 
// Apply the policy globally
app.UseCors("AllowFrontend");  // must come before UseAuthentication/UseAuthorization

Apply per-endpoint with [EnableCors] or disable for specific endpoints with [DisableCors]:

[EnableCors("AllowFrontend")]
[HttpGet("public")]
public IActionResult PublicEndpoint() => Ok();
 
[DisableCors]
[HttpGet("internal")]
public IActionResult InternalEndpoint() => Ok();

Exposed Headers, Simple vs Preflighted, and Caching

  • Reading custom response headers needs WithExposedHeaders. By default browser JS can only read a small all-list of response headers (Content-Type, Cache-Control, etc.). If your client needs to read X-Total-Count, Location, or a custom header, the server must send Access-Control-Expose-Headerspolicy.WithExposedHeaders("X-Total-Count"). This is a frequent “the header is there in DevTools but response.headers.get() returns null” bug.
  • Simple vs preflighted. A request skips the preflight only if it’s a “simple” request: method GET/HEAD/POST, only CORS-safelisted headers, and Content-Type is one of application/x-www-form-urlencoded, multipart/form-data, or text/plain. Anything else (a PUT, an Authorization header, Content-Type: application/json) triggers the OPTIONS preflight.
  • Caching caveat. Preflight results cache per SetPreflightMaxAge, and CORS responses should send Vary: Origin so a shared cache doesn’t serve one origin’s allow-headers to another. Don’t cache credentialed/authorized responses at a shared layer keyed without Origin.

IMPORTANT

CORS is not a server-side authorization mechanism — it only constrains browser JS. curl, Postman, and server-to-server callers ignore it entirely. Always enforce authentication/authorization on the server regardless of CORS configuration.

Pitfalls

AllowAnyOrigin() with AllowCredentials()

What goes wrong: combining AllowAnyOrigin() with AllowCredentials() throws a runtime exception in ASP.NET Core.

Why it happens: the CORS spec prohibits Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true — it would allow any site to make authenticated requests on behalf of the user.

Mitigation: always specify explicit origins when using AllowCredentials(). Use SetIsOriginAllowed(origin => true) only if you genuinely need dynamic origin validation with credentials (rare).

CORS Middleware Order

What goes wrong: CORS headers are missing from responses, causing browser errors even though CORS is configured.

Why it happens: app.UseCors() must be called before app.UseAuthentication(), app.UseAuthorization(), and app.MapControllers(). If it’s placed after, preflight responses don’t include CORS headers.

Mitigation: place app.UseCors() immediately after app.UseRouting() and before any authentication/authorization middleware.

Tradeoffs

  • Wildcard origin vs specific origins: AllowAnyOrigin() is simple for public read APIs serving any client but cannot be combined with AllowCredentials(). Specifying origins with WithOrigins() is required for credentialed requests and reduces attack surface for sensitive endpoints.
  • Global policy vs endpoint-specific: a single global policy is easier to maintain but may be too permissive for write endpoints. Per-endpoint CORS via [EnableCors("PolicyName")] allows tighter control — a public read API can allow any origin while write endpoints restrict to trusted origins.
  • Preflight caching: browsers cache preflight responses to avoid redundant OPTIONS requests. Setting SetPreflightMaxAge(TimeSpan.FromMinutes(10)) in the policy reduces round-trip latency in production. Set it to zero during development to always test current configuration.

Questions

References