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.
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:
- The scheme (
httporhttps) - The host (like
example.com) - The port (like
443or5001)
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 origin | API origin | Same origin? | Needs CORS? |
|---|---|---|---|
https://app.com | https://app.com/api | Yes | No |
https://app.com | https://api.app.com | No (host differs) | Yes |
http://app.com | https://app.com | No (scheme differs) | Yes |
https://app.com | https://app.com:5001 | No (port differs) | Yes |
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.
The table below shows the main headers used in this conversation.
| Header | Sent by | Meaning |
|---|---|---|
Origin | Browser | Which origin is making the call |
Access-Control-Request-Method | Browser (preflight) | The method it wants to use |
Access-Control-Allow-Origin | Server | Which origin is allowed |
Access-Control-Allow-Methods | Server | Which methods are allowed |
Access-Control-Allow-Headers | Server | Which request headers are allowed |
Access-Control-Allow-Credentials | Server | Whether cookies are allowed |
Setting up CORS in ASP.NET Core
There are two steps in every ASP.NET Core app:
- Register the CORS services with
AddCors, and define one or more policies. - 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
Steps
AddCors
register services
Define policy
origins, methods, headers
UseCors
apply in pipeline
Browser allowed
headers sent back
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
Steps
Whole app
UseCors(name)
Controller
[EnableCors]
Action
[EnableCors] / [DisableCors]
Minimal endpoint
RequireCors(name)
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.
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:
| Symptom | Likely cause | Fix on the server |
|---|---|---|
No Allow-Origin header | Origin not in policy | Add it with WithOrigins(...) |
| Works in Postman, fails in browser | Normal CORS behavior | Configure a CORS policy |
| Preflight returns 401 | UseCors after auth | Move UseCors before auth |
| Credentials blocked | Wildcard * with cookies | List origins + AllowCredentials() |
| Custom header rejected | Header not allowed | Add 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
WithOriginsoverAllowAnyOrigin. 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
OPTIONScheck before the real request. - In ASP.NET Core you register policies with
AddCorsand apply them withUseCors,[EnableCors], orRequireCors. - You cannot mix
AllowAnyOrigin()withAllowCredentials(). List real origins, or useSetIsOriginAllowed. - Place
UseCorsafterUseRoutingand before authentication and authorization. - A CORS error is almost always fixed on the server, not in the front-end code.
References and further reading
- Enable Cross-Origin Requests (CORS) in ASP.NET Core — Microsoft Learn
- Cross-Origin Resource Sharing (CORS) — MDN Web Docs
- Cross-Origin Resource Sharing (CORS) in ASP.NET Core: A Comprehensive Guide — Anton Martyniuk
- Enabling CORS in ASP.NET Core By Example — Code Maze
Related Posts
Authentication and Authorization Best Practices in ASP.NET Core
A friendly guide to authentication and authorization in ASP.NET Core for .NET 10 — JWT, cookies, claims, roles, policies, and security best practices with diagrams.
API Key Authentication in ASP.NET Core: The Secure Way
Learn how to add API key authentication to your ASP.NET Core API the right way. Use an AuthenticationHandler, hash keys, compare safely, and follow 2026 security best practices, with diagrams and code.
3 Ways To Create Middleware In ASP.NET Core (Beginner Guide)
Learn the 3 ways to create middleware in ASP.NET Core: inline request delegates, convention-based classes, and factory-based IMiddleware, with simple diagrams and code.
Top 15 Mistakes Developers Make When Creating Web APIs
A warm, beginner-friendly tour of the 15 most common Web API mistakes in ASP.NET Core, with simple fixes, diagrams, tables, and clear C# examples.
Building Secure APIs with Role-Based Access Control in ASP.NET Core
Learn role-based access control (RBAC) in ASP.NET Core. Add roles to JWT tokens, guard endpoints with policies, and return correct 401 and 403 codes, with diagrams and code.
How to Increase the Performance of Web APIs in .NET
A friendly, step-by-step guide to making your ASP.NET Core Web APIs fast: async, caching, query tuning, compression, and pooling in .NET 10.