ASP.NET Core Web API · Cross-Cutting

CORS

The React storefront runs on https://shop.example.com and calls the API on https://api.shop.example.com — a different origin. Browsers block that unless the API says it is allowed. CORS (Cross-Origin Resource Sharing) is how the API says so.

Intermediate 6 min Lesson 25 of 37Examples target .NET 9.
Before you start:JWT Authentication
0/37 in ASP.NET Core Web API
On this page · 6 min · Intermediate

In simple words#

CORS is the building’s guest list for browsers: only websites (origins) on the list may call your API from JavaScript.

Origin
Scheme + domain + port of a site.
CORS policy
Which origins, methods and headers are allowed.
Browser-only
Servers and Postman ignore CORS.

Learning objectives#

  • Explain the same-origin policy and preflight requests
  • Configure a named CORS policy with origins, methods and headers
  • Allow credentials and expose custom headers safely
  • Place UseCors correctly in the pipeline

Explanation#

Browsers only let a page read responses from other origins (scheme + host + port) if the server answers with Access-Control-Allow-Origin for that origin. Non-browser clients (mobile apps, curl, servers) ignore CORS entirely — it is not an access-control mechanism for your data.

Non-simple requests (PUT/DELETE, JSON bodies, Authorization headers) are preceded by a preflight OPTIONS request. The API answers with the allowed methods and headers, and Access-Control-Max-Age lets the browser cache that answer.

Configure named policies: AddCors(o => o.AddPolicy("Frontend", p => p.WithOrigins("https://shop.example.com", "http://localhost:5173").AllowAnyHeader().WithMethods("GET","POST","PUT","DELETE").AllowCredentials())).

AllowCredentials (for cookies) cannot be combined with AllowAnyOrigin. List exact origins, ideally from configuration per environment. WithExposedHeaders("X-Total-Count") lets JavaScript read custom response headers.

Call app.UseCors("Frontend") after UseRouting and before UseAuthentication/UseAuthorization (or use [EnableCors] / RequireCors per endpoint).

Syntax#

C# Illustrative
builder.Services.AddCors(o => o.AddPolicy("Frontend", p => p
    .WithOrigins("https://shop.example.com", "http://localhost:5173")
    .WithMethods("GET", "POST", "PUT", "DELETE")
    .AllowAnyHeader()
    .WithExposedHeaders("X-Total-Count")
    .SetPreflightMaxAge(TimeSpan.FromMinutes(10))));
app.UseCors("Frontend");

Code example: A front-end CORS policy and a preflight#

C#Program.cs Illustrative
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();                                    // keep the demo output clean
builder.Services.AddControllers();
builder.Services.AddCors(options => options.AddPolicy("Frontend", policy => policy
    .WithOrigins("https://shop.example.com", "http://localhost:5173")
    .WithMethods("GET", "POST", "PUT", "DELETE")
    .WithHeaders("Content-Type", "Authorization")
    .WithExposedHeaders("X-Total-Count")
    .AllowCredentials()
    .SetPreflightMaxAge(TimeSpan.FromMinutes(10))));


var app = builder.Build();
app.UseRouting();
app.UseCors("Frontend");
app.MapControllers();
app.Urls.Add("http://127.0.0.1:5225");
await app.StartAsync();


using var http = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:5225") };
string Header(HttpResponseMessage r, string name) =>
    r.Headers.TryGetValues(name, out var v) ? string.Join(",", v) : "-";


foreach (var origin in new[] { "http://localhost:5173", "https://evil.example" })
{
    var request = new HttpRequestMessage(HttpMethod.Get, "/api/products");
    request.Headers.Add("Origin", origin);
    var response = await http.SendAsync(request);
    Console.WriteLine(
quot;GET from
{origin,-22} {(int)response.StatusCode} allow-origin={Header(response, "Access-Control-Allow-Origin")} " +
quot;credentials=
{Header(response, "Access-Control-Allow-Credentials")} expose={Header(response, "Access-Control-Expose-Headers")}");
} var preflight = new HttpRequestMessage(HttpMethod.Options, "/api/products/1"); preflight.Headers.Add("Origin", "https://shop.example.com"); preflight.Headers.Add("Access-Control-Request-Method", "PUT"); preflight.Headers.Add("Access-Control-Request-Headers", "content-type,authorization"); var answer = await http.SendAsync(preflight); Console.WriteLine(
quot;OPTIONS preflight for PUT
{(int)answer.StatusCode} methods={Header(answer, "Access-Control-Allow-Methods")} " +
quot;headers=
{Header(answer, "Access-Control-Allow-Headers")} max-age={Header(answer, "Access-Control-Max-Age")}");
await app.StopAsync(); // Controllers/ProductsController.cs using Microsoft.AspNetCore.Mvc; [ApiController] [Route("api/products")] public class ProductsController : ControllerBase { [HttpGet] public IActionResult GetAll() { Response.Headers["X-Total-Count"] = "2"; return Ok(new[] { "Wireless Mouse", "USB-C Hub" }); } [HttpPut("{id:int}")] public IActionResult Update(int id) => NoContent(); }
Expected output
GET from http://localhost:5173  200 allow-origin=http://localhost:5173 credentials=true expose=X-Total-Count
GET from https://evil.example   200 allow-origin=- credentials=- expose=-
OPTIONS preflight for PUT  204 methods=GET,POST,PUT,DELETE headers=Content-Type,Authorization max-age=600

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. A request from the allowed localhost:5173 origin gets Access-Control-Allow-Origin with that origin, credentials allowed and X-Total-Count exposed.
  2. The same request from evil.example still returns 200 — CORS is enforced by the browser, which will refuse to let that page read the response because no CORS headers were sent.
  3. The browser’s preflight OPTIONS for a PUT with JSON and Authorization gets 204 with the allowed methods and headers.
  4. Max-Age = 600 lets the browser skip preflights for 10 minutes.
  5. UseCors sits after UseRouting so endpoint-specific CORS settings also work.

Real-world use case#

In development the React app runs on localhost:5173; in production on shop.example.com. Both origins come from configuration, so staging and production each allow exactly their own front end.

Common mistakes#

AllowAnyOrigin with credentials

ASP.NET Core refuses this combination; reflecting any origin with credentials would let every site make authenticated calls.

Thinking CORS protects the API

curl, Postman and servers ignore CORS. Authentication and authorization protect data.

UseCors in the wrong place

After UseAuthorization (or after endpoints), preflights can fail with 401/404 and no CORS headers.

Best practices#

  • Exact origins from configuration
  • Only the methods and headers you need
  • Expose only the custom headers clients read
  • Preflight max-age to reduce OPTIONS traffic

Remember this#

Browser enforces, server declares.

Credentials → exact origins, never “*”.

UseRouting → UseCors → UseAuthentication → UseAuthorization.

Practice exercise#

Your task: Read the allowed origins from configuration (Cors:Origins array) instead of hard-coding them.

Need a hint?
var origins = builder.Configuration.GetSection("Cors:Origins").Get<string[]>() ?? []; then WithOrigins(origins).
Show solution
C# Illustrative
// appsettings.json
{
  "Cors": { "Origins": [ "http://localhost:5173", "https://shop.example.com" ] }
}


// Program.cs
var builder = WebApplication.CreateBuilder(args);
var origins = builder.Configuration.GetSection("Cors:Origins").Get<string[]>() ?? [];
builder.Services.AddCors(options => options.AddPolicy("Frontend", policy => policy
    .WithOrigins(origins)
    .AllowAnyHeader()
    .AllowAnyMethod()));
var app = builder.Build();
app.UseCors("Frontend");
app.Run();

Mini quiz#

CORS quizMultiple choice Question 1 of 3

Who enforces CORS?

Score so far: 0/0

Want more? Practise all ASP.NET Core Web API MCQs · ASP.NET Core Web API interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.