Browsers refuse to let a page on one origin read responses from another origin unless the server explicitly allows it. This tutorial covers how to set up CORS policies in ASP.NET Core Web API — the default policy for development, named policies for real clients, method/header restrictions, per-endpoint policies, and how to see it working with plain curl.

What is CORS?
CORS (Cross-Origin Resource Sharing) is a W3C standard that lets a server relax the same-origin policy the browser enforces. With the right CORS policy, the clients you expect can call your Web API from the browser. Without one, the client sees this in the console:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

One thing worth understanding up front: CORS is not security for your API. The server always processes the request — CORS only controls whether the browser lets the page read the response. Tools like curl and Postman ignore it entirely. Protect your API with authentication; use CORS to declare which web apps may call it.
Same-origin policy
Two URLs share an origin only when the scheme, host, and port are all identical.
Same origin:
https://geekstore.com/productshttps://geekstore.com/orders
Different origins from https://geekstore.com/products:
https://api.geekstore.com/products— different hosthttp://geekstore.com/products— different schemehttps://geekstore.com:9810/products— different port
A React or Angular app served from https://geekstore.com calling an API at https://api.geekstore.com is a cross-origin call — that is the everyday case CORS exists for.
AddDefaultPolicy
If one policy is enough for the whole application, register it as the default. A wide-open policy is fine during development:
var builder = WebApplication.CreateBuilder(args);
if (builder.Environment.IsDevelopment())
{
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
});
});
}
var app = builder.Build();
app.UseCors();
app.MapControllers();
app.Run();
Call UseCors() after UseRouting() (implicit in minimal hosting) and before MapControllers() — middleware order matters here.
AddPolicy — named policies for real clients
In production you allow the specific origins that host your front ends and nothing else:
const string policyName = "GeeksPolicy";
builder.Services.AddCors(options =>
{
options.AddPolicy(policyName, policy =>
{
policy.WithOrigins("https://geekstore.com", "https://geeks.com")
.AllowAnyMethod()
.AllowAnyHeader();
});
});
var app = builder.Build();
app.UseCors(policyName);
You can watch the policy work without a browser. Send a request with an Origin header from an allowed origin and the response carries the CORS header:
curl -i -H "Origin: https://geekstore.com" http://localhost:5000/api/products
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://geekstore.com
Send the same request from an origin that is not in the policy and the header is simply absent — which is what makes the browser block the page from reading the response:
curl -i -H "Origin: https://evil.com" http://localhost:5000/api/products
HTTP/1.1 200 OK
# no Access-Control-Allow-Origin header
Preflight requests
For anything beyond simple GET/POST — a PUT, a DELETE, or a custom header — the browser first sends an OPTIONS "preflight" request asking for permission. ASP.NET Core answers it automatically from your policy:
curl -i -X OPTIONS http://localhost:5000/api/products \
-H "Origin: https://geekstore.com" \
-H "Access-Control-Request-Method: PUT"
HTTP/1.1 204 No Content
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Origin: https://geekstore.com
If the preflight comes back without those headers, the browser never sends the real request — that is usually where "my PUT works in Postman but fails in the browser" comes from.
Allow subdomains with wildcards
WithOrigins("https://geekstore.com") allows exactly that host — https://m.geekstore.com is still blocked. To allow every subdomain:
options.AddPolicy(policyName, policy =>
{
policy.WithOrigins("https://*.geekstore.com")
.SetIsOriginAllowedToAllowWildcardSubdomains();
});
Restrict HTTP methods
If an origin should only read data, allow only GET:
options.AddPolicy(policyName, policy =>
{
policy.WithOrigins("https://geekstore.com")
.WithMethods("GET")
.AllowAnyHeader();
});
Add the write methods only for origins that need them:
policy.WithOrigins("https://geekstore.com")
.WithMethods("GET", "PUT", "DELETE");
Restrict request headers
WithHeaders lists the headers a cross-origin client is allowed to send — standard ones like Content-Type, or custom ones like an API key:
using Microsoft.Net.Http.Headers;
options.AddPolicy(policyName, policy =>
{
policy.WithOrigins("https://geekstore.com")
.WithHeaders(HeaderNames.ContentType, "ApiKey");
});
The client then sends:
var client = new HttpClient();
client.DefaultRequestHeaders.Add("ApiKey", "0399-09944");
Multiple policies
Different clients often need different rules — a full-access website and a read-only mobile web app, for example:
builder.Services.AddCors(options =>
{
options.AddPolicy("WebsitePolicy", policy =>
{
policy.WithOrigins("https://geekstore.com")
.AllowAnyHeader()
.AllowAnyMethod();
});
options.AddPolicy("MobilePolicy", policy =>
{
policy.WithOrigins("https://m.geekstore.com")
.WithMethods("GET")
.AllowAnyHeader();
});
});
Per-endpoint policies with RequireCors
The middleware call app.UseCors(policyName) applies one policy to every request. To scope a policy to specific endpoints, attach it with RequireCors:
var app = builder.Build();
app.UseCors();
app.MapGet("/products", () => Results.Ok(new[] { "products" }))
.RequireCors("GeeksPolicy");
app.MapControllers()
.RequireCors("GeeksPolicy");
EnableCors on a controller or action
The attribute form does the same thing declaratively. Name a policy to use it, or leave the attribute empty for the default policy:
[EnableCors("Products")]
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
}
It works on individual action methods too, so one action can be open to a partner site while the rest of the controller stays locked down.
DisableCors
When middleware applies CORS everywhere, you can carve out exceptions — an endpoint that must never be called cross-origin:
[DisableCors]
[HttpGet("profit")]
public IActionResult CalculateProductProfit()
{
...
}
Quick checklist
- Register policies with
AddCorsbeforebuilder.Build(); callUseCorsbeforeMapControllers. - Allow exact origins — scheme, host, and port must all match, and no trailing slash.
- Development can use
AllowAnyOrigin; production should list real origins. - Remember CORS protects browser users, not the API itself — pair it with authentication.
Comments (0)
No comments yet — be the first to share your thoughts.