← Back to all posts

CORS in .NET

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:

  1. A user opens https://app.contoso.com
  2. JavaScript running in the browser calls https://api.contoso.com
  3. The browser checks whether api.contoso.com allows the origin app.contoso.com

This backend-to-backend flow is not affected by CORS:

  1. ServiceA receives a request
  2. ServiceA uses HttpClient to call ServiceB
  3. 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.com
  • https://api.contoso.com
  • https://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 CORS
  • HttpClient from 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 HttpClient to 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 PUT or DELETE
  • custom headers
  • Authorization headers
  • 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() with AllowCredentials()

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, and AllowAnyHeader in production without need
  • debugging only the main request and ignoring the preflight OPTIONS request
  • 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