CORS

Why the browser blocks cross-origin reads, how to grant one origin an exception, and why CORS is not authorisation.

Syntaxbuilder.Services.AddCors(options => options.AddPolicy("spa", policy => policy.WithOrigins("https://app.example.com")));

A page served from https://app.example.com can send a request to https://api.example.com, but the browser hides the response from the page's JavaScript unless the API says that page may read it. That restriction is the same-origin policy. An origin is a scheme, a host and a port taken together, so https://example.com and http://example.com are different origins. CORS, Cross-Origin Resource Sharing, is the set of response headers that grant an exception.

Who enforces it

Only the browser. By the time the error appears in the console your handler has already run. curl, a mobile app and a background job never see a CORS error. So CORS is not a security check on your server: it decides what one web page's script may read, nothing more. Authentication and authorisation still do all the real work.

Configuring a policy

Register named policies with AddCors, then turn the middleware on with app.UseCors("name"). It must run after routing and before authorisation. The shorthand is AddDefaultPolicy plus a bare app.UseCors(), which applies one policy everywhere; the longhand names a policy and lets a single endpoint opt in with .RequireCors("name"). Both produce the same headers.

Credentials

AllowCredentials() lets cookies and Authorization headers ride along. Combining it with AllowAnyOrigin() is rejected: the CORS service returns an invalid response instead of a usable header, because a wildcard plus credentials lets any site act as your signed-in user. Faking it with SetIsOriginAllowed(_ => true) is the same mistake spelled differently.

Example

Example · csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("spa", policy =>
    {
        // Scheme, host and port. No trailing slash, no path.
        policy.WithOrigins("https://app.example.com", "http://localhost:5173")
            .WithMethods("GET", "POST")
            .WithHeaders("Content-Type", "Authorization")
            // Cache the preflight answer so the browser stops re-asking.
            .SetPreflightMaxAge(TimeSpan.FromMinutes(10));
    });
});

var app = builder.Build();

// After routing, before authorisation.
app.UseCors("spa");

app.MapGet("/orders", () => new[] { "order-1001" });

app.Run();

When to use it

  • Let a React app on its own domain call your API without the browser blocking the response.
  • Decide whether a failing fetch is a CORS policy problem or an authentication problem.
  • Expose one public status endpoint to any site while keeping the rest of the API same-origin.

More examples

Allowing cookies across origins

Two policies side by side: the one that works names its origins, the one that does not tries to allow everything at once. Reach for AllowCredentials when a single-page app on another host signs in with a cookie, and always list the origins explicitly when you do.

Example · csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("spa-with-cookies", policy =>
    {
        policy.WithOrigins("https://app.example.com")
            .AllowAnyHeader()
            .AllowAnyMethod()
            // Required before the browser will send the session cookie.
            .AllowCredentials();
    });

    // This policy is refused at runtime: a wildcard origin plus credentials
    // would let any site read data as the signed-in user, so the CORS service
    // returns an invalid response instead of a working header.
    options.AddPolicy("broken", policy =>
    {
        policy.AllowAnyOrigin().AllowCredentials();
    });
});

var app = builder.Build();
app.UseCors("spa-with-cookies");

app.MapGet("/me", (HttpContext http) => http.User.Identity?.Name ?? "anonymous");

app.Run();

One public endpoint, the rest closed

RequireCors attaches a policy to one endpoint, which keeps a wide-open policy from leaking onto routes that return private data. AllowAnyOrigin is acceptable here only because no credentials are involved and the data is already public. Reach for this shape when you publish a status or price endpoint for anyone to embed.

Example · csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    // Only a named policy, and no default policy, so nothing is granted globally.
    options.AddPolicy("public-read", policy => policy.AllowAnyOrigin().WithMethods("GET"));
});

var app = builder.Build();

// The CORS middleware still has to run, or the policy attached to an endpoint is
// never applied. With no argument and no default policy it grants nothing by
// itself: each endpoint opts in with RequireCors.
app.UseCors();

app.MapGet("/status", () => new { Status = "ok" })
    .RequireCors("public-read");

// Reachable by your own pages and by server-side clients, but a browser script
// on another origin cannot read the response.
app.MapGet("/orders", () => new[] { "order-1001" });

app.Run();

Reading the handshake yourself

This is the fastest way to tell a CORS problem from an authorisation problem. If curl gets the data and only the header is missing, your policy is wrong. If curl gets a 401, CORS was never the issue. It also shows plainly that the server ran the request either way.

Example · bash
# A preflight: the browser asks before sending the real request.
curl -i -X OPTIONS https://api.example.com/orders \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST"
# 204, with Access-Control-Allow-Origin and Access-Control-Allow-Methods.

# An origin that is not on the list: the body still comes back.
curl -i https://api.example.com/orders -H "Origin: https://evil.example"
# 200 and the JSON, but no Access-Control-Allow-Origin header.
# curl prints the data; a browser would refuse to hand it to the script.

Discussion

  • Be the first to comment on this lesson.