Skip to main content
SEMastery
ASP.NETbeginner

CORS in ASP.NET Core: A Comprehensive Guide

A simple, friendly guide to CORS in ASP.NET Core. Learn how the browser, preflight requests, and policies work, with clear diagrams, tables, and code.

11 min readUpdated April 10, 2026

A guard at your apartment gate

Imagine you live in a housing society in a big city. At the main gate there is a guard. When a delivery person comes for you, the guard does not just let them walk in. The guard checks a list: "Is this person allowed to visit flat 402?" If your name and their name match the visitor list, they are allowed in. If not, they are turned away at the gate.

CORS works almost exactly like that guard. Your browser is the guard. Your API is the flat. When a web page from one website (say, shop.com) tries to call an API on another website (say, api.shop.com), the browser stops and asks: "Is this page on the allowed visitor list?" Only if the API server says "yes, that origin is welcome" will the browser hand the response back to the page.

CORS stands for Cross-Origin Resource Sharing. It sounds scary, but it is just a polite set of rules that let one website safely read data from another website. In this guide we will learn what an "origin" is, why the browser blocks things, how the famous "preflight" check works, and exactly how to set it all up in ASP.NET Core.

What is an "origin"?

An origin is made of three parts joined together:

  1. The scheme (http or https)
  2. The host (like example.com)
  3. The port (like 443 or 5001)

If even one of these three is different, the browser treats it as a different origin. This surprises many beginners. https://example.com and http://example.com are different origins, because the scheme changed. https://example.com and https://example.com:5001 are different too, because the port changed.

The browser follows a rule called the same-origin policy. By default, code on one origin cannot read responses from a different origin. CORS is the official way to relax that rule, just a little, on purpose.

Page originAPI originSame origin?Needs CORS?
https://app.comhttps://app.com/apiYesNo
https://app.comhttps://api.app.comNo (host differs)Yes
http://app.comhttps://app.comNo (scheme differs)Yes
https://app.comhttps://app.com:5001No (port differs)Yes
The browser compares the page origin and the API origin before allowing the response through.

Why CORS exists at all

You may wonder why browsers are so strict. The reason is your safety as a user.

When you log in to your bank, the bank stores a cookie in your browser. Now imagine you open a different, evil website in another tab. Without the same-origin policy, that evil page could quietly send a request to your bank, reuse your cookie, and read your account balance. That would be terrible.

The same-origin policy stops this. By default, the evil page cannot read your bank's responses. CORS then gives the bank a careful way to say "these specific friendly websites are allowed, and nobody else." So CORS is not there to annoy you. It is a seatbelt.

A key thing to understand: CORS is enforced by the browser, not by your server. Tools like Postman or curl do not care about CORS at all. That is why an API call works in Postman but fails in the browser. The server is fine; the browser is just protecting the user.

Simple requests vs preflight requests

The browser splits cross-origin calls into two kinds.

A simple request is a basic GET, HEAD, or POST that uses only ordinary headers and a plain content type. For these, the browser sends the real request straight away and checks the response headers afterward.

A preflighted request is anything more advanced, like a PUT, a DELETE, a request with a custom header (for example Authorization or X-API-Key), or a JSON body. Before sending the real request, the browser first sends a small "permission check" called a preflight. This preflight uses the OPTIONS method and asks the server: "I want to send a PUT with these headers. Is that allowed?"

Only if the server replies "yes" does the browser then send the real request.

A preflight OPTIONS request happens before the real request when the call is not 'simple'.

The table below shows the main headers used in this conversation.

HeaderSent byMeaning
OriginBrowserWhich origin is making the call
Access-Control-Request-MethodBrowser (preflight)The method it wants to use
Access-Control-Allow-OriginServerWhich origin is allowed
Access-Control-Allow-MethodsServerWhich methods are allowed
Access-Control-Allow-HeadersServerWhich request headers are allowed
Access-Control-Allow-CredentialsServerWhether cookies are allowed

Setting up CORS in ASP.NET Core

There are two steps in every ASP.NET Core app:

  1. Register the CORS services with AddCors, and define one or more policies.
  2. Turn on the CORS middleware with UseCors, choosing which policy to apply.

Here is the smallest possible setup using a named policy. This is the style you should prefer in real projects.

var builder = WebApplication.CreateBuilder(args);
 
const string FrontendPolicy = "FrontendPolicy";
 
builder.Services.AddCors(options =>
{
    options.AddPolicy(FrontendPolicy, policy =>
    {
        policy.WithOrigins("https://app.shop.com", "https://admin.shop.com")
              .WithMethods("GET", "POST", "PUT", "DELETE")
              .WithHeaders("Content-Type", "Authorization");
    });
});
 
var app = builder.Build();
 
app.UseRouting();
app.UseCors(FrontendPolicy); // after UseRouting, before auth
app.UseAuthentication();
app.UseAuthorization();
 
app.MapControllers();
app.Run();

Notice three good habits here. We listed exact origins instead of allowing everyone. We listed the methods and headers the front end actually uses. And we placed UseCors in the right spot in the pipeline.

The two-step CORS setup

AddCors
Define policy
UseCors
Browser allowed

Steps

1

AddCors

register services

2

Define policy

origins, methods, headers

3

UseCors

apply in pipeline

4

Browser allowed

headers sent back

Register a policy, then apply it with middleware.

Default policy vs named policy

You can also set a default policy. The default policy applies automatically when you call UseCors() with no name. A named policy must be chosen by name, which gives you more control when different parts of your API need different rules.

builder.Services.AddCors(options =>
{
    // Default policy: used by UseCors() with no name
    options.AddDefaultPolicy(policy =>
    {
        policy.WithOrigins("https://app.shop.com")
              .AllowAnyHeader()
              .AllowAnyMethod();
    });
 
    // Named policy: used by UseCors("AdminOnly") or [EnableCors("AdminOnly")]
    options.AddPolicy("AdminOnly", policy =>
    {
        policy.WithOrigins("https://admin.shop.com")
              .AllowAnyHeader()
              .AllowAnyMethod()
              .AllowCredentials();
    });
});

For a quick mental model: use the default policy when most of your API shares the same rules, and add named policies for the special cases.

Applying CORS per endpoint

In bigger apps, you may not want CORS on every endpoint. ASP.NET Core lets you attach a policy to a single controller, a single action, or a single minimal API endpoint.

With controllers you use the [EnableCors] and [DisableCors] attributes:

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    [HttpGet]
    [EnableCors("FrontendPolicy")] // CORS only on this action
    public IActionResult GetAll() => Ok(new[] { "order-1", "order-2" });
 
    [HttpDelete("{id}")]
    [DisableCors] // never allow cross-origin delete
    public IActionResult Delete(int id) => NoContent();
}

With minimal APIs you chain RequireCors:

app.MapGet("/products", () => new[] { "pen", "book" })
   .RequireCors("FrontendPolicy");

Where a CORS policy can be applied

Whole app
Controller
Action
Minimal endpoint

Steps

1

Whole app

UseCors(name)

2

Controller

[EnableCors]

3

Action

[EnableCors] / [DisableCors]

4

Minimal endpoint

RequireCors(name)

From the whole app down to a single endpoint.

The credentials trap

This is the single most common mistake, so let us slow down.

If your front end sends cookies or uses the Authorization header in a way the browser treats as credentials, the server must add AllowCredentials(). But there is a strict rule: you cannot combine AllowAnyOrigin() with AllowCredentials(). The browser will reject it.

Why? Because AllowAnyOrigin() sends Access-Control-Allow-Origin: *, and the CORS standard forbids the wildcard * together with credentials. Allowing any site to send a user's cookies would be a security hole.

So when you need cookies, you must list real origins:

builder.Services.AddCors(options =>
{
    options.AddPolicy("WithCookies", policy =>
    {
        policy.WithOrigins("https://app.shop.com") // real origin, not *
              .AllowAnyHeader()
              .AllowAnyMethod()
              .AllowCredentials();               // cookies allowed
    });
});

If you truly must accept many origins and credentials (for example a dynamic list of tenant domains), do not reach for AllowAnyOrigin(). Instead use SetIsOriginAllowed and check the origin yourself:

policy.SetIsOriginAllowed(origin =>
       {
           var host = new Uri(origin).Host;
           return host.EndsWith(".shop.com"); // only your sub-domains
       })
      .AllowAnyHeader()
      .AllowAnyMethod()
      .AllowCredentials();

This is safe because you decide, per request, whether the origin is trusted. Never write SetIsOriginAllowed(_ => true) together with credentials in production unless you fully understand the risk.

Decision path for picking the right origin setting.

Middleware order matters

The order of middleware in ASP.NET Core is not random. For CORS, the rule is simple and important:

Call UseCors after UseRouting and before UseAuthentication and UseAuthorization. If you put UseCors too late, the preflight OPTIONS request may be handled by authentication first, get a 401, and never receive the CORS headers. The browser then shows a confusing CORS error even though the real problem is order.

app.UseRouting();
app.UseCors("FrontendPolicy"); // right here
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

A handy way to remember it: route, then CORS, then check who you are, then check what you can do.

Reading CORS errors without panic

When something fails, the browser console shows a message like "No Access-Control-Allow-Origin header is present on the requested resource." Beginners often try to fix this in the front-end fetch call. That almost never works, because CORS is a server decision.

Here is a calm checklist to debug it:

SymptomLikely causeFix on the server
No Allow-Origin headerOrigin not in policyAdd it with WithOrigins(...)
Works in Postman, fails in browserNormal CORS behaviorConfigure a CORS policy
Preflight returns 401UseCors after authMove UseCors before auth
Credentials blockedWildcard * with cookiesList origins + AllowCredentials()
Custom header rejectedHeader not allowedAdd it with WithHeaders(...)

Open your browser's network tab, find the failed request, and look at the response headers. If the right Access-Control-Allow-* headers are missing, the server policy is the place to fix it.

A small but useful extra: preflight caching

Each preflight is an extra round trip, which can slow things down. You can tell the browser to remember the preflight result for a while using SetPreflightMaxAge. This reduces repeated OPTIONS calls.

options.AddPolicy("Cached", policy =>
{
    policy.WithOrigins("https://app.shop.com")
          .AllowAnyHeader()
          .AllowAnyMethod()
          .SetPreflightMaxAge(TimeSpan.FromMinutes(10));
});

Browsers cap this value, so do not expect very long caching, but a sensible SetPreflightMaxAge still helps busy APIs.

Security best practices

CORS is a safety feature, so configure it safely:

  • Prefer WithOrigins over AllowAnyOrigin. List only the sites you trust.
  • Keep your origin list in configuration, so you can change it without recompiling.
  • Only allow the methods and headers your front end really uses.
  • Never combine wildcard origins with credentials.
  • Remember that CORS protects users in the browser. It is not a replacement for authentication and authorization. You still need those.

Quick recap

  • CORS is a browser rule that controls which origins can read your API's responses.
  • An origin is scheme + host + port. Change any one, and it counts as a different origin.
  • The browser blocks cross-origin reads by default; the server uses Access-Control-Allow-* headers to grant access.
  • Hard or unusual requests get a preflight OPTIONS check before the real request.
  • In ASP.NET Core you register policies with AddCors and apply them with UseCors, [EnableCors], or RequireCors.
  • You cannot mix AllowAnyOrigin() with AllowCredentials(). List real origins, or use SetIsOriginAllowed.
  • Place UseCors after UseRouting and before authentication and authorization.
  • A CORS error is almost always fixed on the server, not in the front-end code.

References and further reading

Related Posts