Know Further About Cross-Origin Request ASP.NET Core – Windows ASP.NET Core Hosting 2024 | Review and Comparison

Cross-Origin Requests are a part of the HTTP specification that has driven many developers to the limits of their sanity for how seemingly complex it is to make two websites talk with each other. In principle CORS actually exists to make this easier: it allows an opt-out for the same-origin policy. This means that without CORS it would be absolutely impossible to make an API call to another domain.

So, apparently CORS is here to help so what does it? The CORS specification allows a server to set headers that indicate with part of the same-origin policy should not be enforced. This enforcement however happens in the browser, so on the client side. This is properly the biggest source of confusion around CORS: many people either try to fix CORS errors in JavaScript (which has nothing to say in the matter) or are trying to find what part of their backend is failing (which doesn’t do the failing). In this article I will show how CORS works and what you need to do to make a cross-domain request happen.

Preflight and HTTP OPTIONS

CORS request fall in either one of two categories: simple requests and non-simple requests. For the non-simple request the browser will make a preflight request to ask the server if the main request will be allowed. For simple requests the browser just goes ahead with the request and only rejects the call afterwards. A full definition of what qualifies as a simple request and what doesn’t can be found on the excellent MDN page about this topic.

The server allows access from other domains by sending the Access-Control-Allow-* headers. The most important one is the Access-Control-Allow-Origin header which allows access to the URL from other domains, but there are more access control headers. When a preflight happens you will also see the Access-Control-Request-* headers which are used to tell the server which access controls a client wants. Those are headers the client sends, but you will never have to set them yourself and they also don’t grand you the actual access.

The example below shows a simple implementation in Dotnet Core. Dotnet has better ways to configure CORS then directly setting headers in the controllers, but I want to first show you the most basic low-level implementation without involving a lot of framework magic.

public class HomeController : Controller
{
    [HttpGet("/api/v1/foo")]
    public IActionResult Api() 
    {
        Response.Headers.Add("Access-Control-Allow-Origin", "http://localhost:8080");
        return Ok(new {
            Foo = "Foo",
        });
    }
}
  const res = await fetch('http://localhost:5000/api/v1/foo')
  const json = await res.json()
  console.log(json)

In this example the following things happen:

  • the browser makes the HTTP GET request to the server.
  • the server sends back the response (headers followed by the body as is normal).
  • as soon as the headers are in the browser validates the value of the Access-Control-Allow-Origin header with the current origin of the website and if they don’t matches it will reject the response.

The next example show a CORS request with a preflight in action. Note here that we have two actions: one for the HTTP OPTIONS preflight call and one for the HTTP GET ‘normal’ call.

public class HomeController : Controller
{
    [HttpOptions("/api/v1/foo")]
    public IActionResult ApiOptions() 
    {
        Response.Headers.Add("Access-Control-Allow-Origin", "http://localhost:8080");
        Response.Headers.Add("Access-Control-Allow-Headers", "myheader");
        return Ok();
    } 
        
    [HttpGet("/api/v1/foo")]
    public IActionResult Api() 
    {
        Response.Headers.Add("Access-Control-Allow-Origin", "http://localhost:8080");
        return Ok(new {
            Foo = "Foo",
        });
    }
}
const res = await fetch(
  'http://localhost:5000/api/v1/foo', 
  {headers: {myheader: 'foo'}}
)
const json = await res.json()
console.log(json)

In this example there are a few more things that happen because the browser needs to make a preflight request because of the custom header myheader:

  • the browser makes a HTTP OPTIONS request to the server to validate if the request will be allowed. In this request it will include two headers for the access control: Access-Control-Request-Headers: myheader and Access-Control-Request-Method: GET.
  • the browser responses with the Access-Control-Allow headers for the things the browser has send as a Access-Control-Request header: Access-Control-Allow-Headers: myheader and Access-Control-Allow-Origin: http://localhost:8080. We don’t have to send an allow header for the method because a GET is already allowed by default. If the granted access controls don’t match the requested ones the browser will reject the API call.
  • If they however do match, the browser will make the regular HTTP GET request.
  • as soon as the headers are in the browser validates the value of the Access-Control-Allow-Origin with the current origin of the website and if they don’t matches it will reject the response. Note that it just validates the origin again and not any of the other Access-Control-Allow headers.

Dotnet Core CORS Policies

As already mentioned there are better ways to manage CORS in Dotnet Core then by manually setting headers all over the place. As described in the linked documentation there are multiple ways to implement CORS. The way I recommend is to use named policies with an attribute on the actions. In this way you have all the configuration in one place, but you still have an explicit whitelist of endpoints that are accessible from other domains. It also allows you to have a different config for different domains. In my opinion this is the best combination of security and convenience.

The first thing we need to do is to add a named policy in the Startup of our application. In addition to this we also need to call UseCors on the app. The name of the policy is defined in a constant so we can use it on other places to reference the policy we created:

public class Startup
{
    public const string FRONTEND_CORS = "FRONTEND_CORS";
    
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options => 
            options.AddPolicy(
                FRONTEND_CORS,
                options => options
                    .WithOrigins("http://localhost:8080")
                    .WithHeaders("myheader")
            )
        );
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseCors();
    }
}

To use this policy we can annotate an action (or a controller) with the EnableCors attribute. When using named policies we need to supply it with the name of the policy, or it will try to find a default policy (something which we didn’t setup).

public class HomeController : Controller
{       
    [EnableCors(Startup.FRONTEND_CORS)]
    [HttpGet("/api/v1/foo")]
    public IActionResult Api() 
    {
        return Ok(new {
            Foo = "Foo",
        });
    }
}

Conclusion

I hope this article has given you some insight on how CORS works and where to look when you see the ‘Access to fetch at…’ errors appear in your console, as well as show you how to configure CORS in a proper way in a Dotnet Core application.