All articles

How to Setup CORS Policies in ASP.NET Core Web API

Configure CORS in ASP.NET Core Web API — default and named policies, origin/method/header restrictions, preflight requests, per-endpoint policies with RequireCors, and EnableCors/DisableCors attribute

0 · log in to like, save & follow Share on LinkedIn Share on X

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.

How CORS works between a browser and ASP.NET Core Web API

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.

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/products
  • https://geekstore.com/orders

Different origins from https://geekstore.com/products:

  • https://api.geekstore.com/products — different host
  • http://geekstore.com/products — different scheme
  • https://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 AddCors before builder.Build(); call UseCors before MapControllers.
  • 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.
Enjoyed this article? Get the best GeeksArray articles in your inbox — once a week, no spam, unsubscribe anytime.

Comments (0)

Log in to join the conversation.

No comments yet — be the first to share your thoughts.