CORS in .NET
Reading Time: 7 minutes
The Problem
Cross-Origin Resource Sharing, or CORS, is one of those topics that causes confusion because teams often discover it only after something stops working.
A common setup looks like this:
- ServiceA hosts a frontend, BFF, or another API
- ServiceB exposes an API on a different origin
- requests start failing in the browser even though ServiceB is healthy
The confusion usually starts with this question:
If ServiceA needs to communicate with ServiceB, does CORS affect that call?
The real answer is: it depends on who is making the request.
If the request comes from a browser, CORS matters.
If the request comes from one backend service to another backend service, CORS does not apply.
That distinction is the entire topic.
For example, this browser-based flow is affected by CORS:
- A user opens
https://app.contoso.com - JavaScript running in the browser calls
https://api.contoso.com - The browser checks whether
api.contoso.comallows the originapp.contoso.com
This backend-to-backend flow is not affected by CORS:
- ServiceA receives a request
- ServiceA uses
HttpClientto call ServiceB - ServiceB responds directly to ServiceA
No browser is enforcing origin rules in that second case.
The Solution
Treat CORS as a browser security policy, not as a general network security feature.
In practical terms:
- configure CORS on ServiceB when a browser-based client from ServiceA needs to call it directly
- do not expect CORS to protect backend-to-backend traffic
- use authentication, authorization, network controls, and API gateway policies for service-to-service security
For .NET applications, CORS support is built into ASP.NET Core. In .NET 10, no extra NuGet package is required when you are using the standard ASP.NET Core shared framework.
Description
What CORS Actually Does
CORS allows a server to tell the browser which cross-origin requests are allowed.
An origin is a combination of:
- scheme
- host
- port
That means these are different origins:
https://app.contoso.comhttps://api.contoso.comhttps://app.contoso.com:5001
Even if both services belong to the same company, the browser still treats them as cross-origin if those parts differ.
Important rule
CORS is enforced by browsers, not by ASP.NET Core itself and not by backend services calling each other.
That means:
fetch()from the browser to another origin can be blocked by CORSHttpClientfrom ServiceA to ServiceB is not blocked by CORS- Postman and curl are not governed by browser CORS rules
This is why a request can work in Postman but fail in the browser.
ServiceA to ServiceB: What Changes?
The answer depends on the architecture.
Scenario 1: Browser in ServiceA calls ServiceB directly
Example:
- ServiceA serves a React, Blazor WebAssembly, or Astro frontend from
https://app.contoso.com - the browser calls ServiceB at
https://api.contoso.com
In this case, ServiceB must allow ServiceA’s origin through CORS.
If it does not, the browser blocks the call before your application can use the response.
Scenario 2: ServiceA backend calls ServiceB backend
Example:
- ServiceA is an ASP.NET Core API or BFF
- ServiceA uses
HttpClientto call ServiceB - the browser only talks to ServiceA
In this case, CORS between ServiceA and ServiceB is irrelevant.
Why? Because this is server-to-server communication. The browser is not in that hop.
That means the real concerns are:
- authentication
- authorization
- timeouts and retries
- TLS
- service discovery or DNS
- network rules
Not CORS.
Scenario 3: Mixed flow
Some systems do both:
- browser calls ServiceA
- browser also calls ServiceB directly for some features
- ServiceA also calls ServiceB on the backend
In that case:
- direct browser-to-ServiceB calls need CORS
- backend-to-backend calls do not
Configuring CORS in .NET 10
For standard ASP.NET Core apps on .NET 10, CORS is built in. No additional NuGet package is required.
Configure a named CORS policy
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy("FrontendPolicy", policy =>
{
policy
.WithOrigins("https://app.contoso.com")
.WithMethods("GET", "POST")
.WithHeaders("content-type", "authorization");
});
});
builder.Services.AddEndpointsApiExplorer();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseCors("FrontendPolicy");
app.MapGet("/orders/{id:int}", (int id) =>
{
return Results.Ok(new { id, status = "Shipped" });
});
app.Run();
This policy allows browser requests from https://app.contoso.com to access the API.
Why named policies are better
Named policies make intent obvious and reduce the chance of accidentally opening the API too widely.
That matters because these are very different:
- allow one known frontend origin
- allow any origin
- allow credentials
Those choices should be deliberate.
Understanding Preflight Requests
Some cross-origin requests trigger a browser preflight request using OPTIONS.
This happens when the browser needs to ask first whether the real request is allowed, especially when using:
- non-simple methods such as
PUTorDELETE - custom headers
Authorizationheaders- JSON requests in some scenarios depending on request shape
The browser sends an OPTIONS request first. If the server does not answer with the expected CORS headers, the actual call never happens.
This is why teams sometimes say, “my API endpoint is fine, but the browser never reaches it.” The request may be failing at preflight.
CORS with Credentials
If ServiceA’s browser client sends cookies or authenticated cross-origin requests, the policy must be stricter.
builder.Services.AddCors(options =>
{
options.AddPolicy("FrontendWithCookies", policy =>
{
policy
.WithOrigins("https://app.contoso.com")
.AllowCredentials()
.AllowAnyHeader()
.AllowAnyMethod();
});
});
Important rule:
- do not combine
AllowAnyOrigin()withAllowCredentials()
ASP.NET Core prevents this because it would be an unsafe configuration.
Service-to-Service Security Is Not CORS
If ServiceA calls ServiceB with HttpClient, secure that path using normal service security.
Example registration in .NET:
builder.Services.AddHttpClient<ServiceBClient>(client =>
{
client.BaseAddress = new Uri("https://serviceb.internal/");
client.Timeout = TimeSpan.FromSeconds(10);
});
Example client:
public sealed class ServiceBClient
{
private readonly HttpClient _httpClient;
public ServiceBClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<OrderDto?> GetOrderAsync(int orderId, CancellationToken ct = default)
{
return await _httpClient.GetFromJsonAsync<OrderDto>($"orders/{orderId}", ct);
}
}
That call is not governed by CORS. It is governed by whether ServiceB accepts the request and whether the network path exists.
Common Mistakes
- assuming CORS protects internal APIs from unauthorized backend callers
- enabling
AllowAnyOrigin,AllowAnyMethod, andAllowAnyHeaderin production without need - debugging only the main request and ignoring the preflight
OPTIONSrequest - thinking CORS failures mean the API is down
- mixing browser security concerns with service-to-service security design
A Good Mental Model
Use this shortcut:
- browser to API across origins: think CORS
- server to server: think authentication, authorization, networking, and resiliency
That one rule prevents most CORS design mistakes.
Summary
CORS in .NET matters when a browser-based client from ServiceA needs to call ServiceB across origins.
It does not govern backend-to-backend communication between ServiceA and ServiceB.
For .NET applications:
- configure explicit CORS policies on APIs that browsers call directly
- keep allowed origins narrow
- understand preflight behavior
- do not treat CORS as service-to-service security
If the caller is a browser, CORS is part of the design. If the caller is another service, it is not.
References
- Enable Cross-Origin Requests (CORS) in ASP.NET Core: https://learn.microsoft.com/aspnet/core/security/cors
- HTTP requests in .NET with IHttpClientFactory: https://learn.microsoft.com/dotnet/core/extensions/httpclient-factory
- Cross-origin resource sharing (CORS): https://developer.mozilla.org/docs/Web/HTTP/CORS