How CORS Works? Bad and Best Practices for CORS Configuration

With Examples in ASP.NET Core

Sasha Marfut
Level Up Coding

--

Photo by Sigmund on Unsplash

In this article, we will discuss what CORS is, how browsers implement it, and how to avoid common issues when configuring CORS in ASP.NET Core.

Let’s say we, full stack developers, started developing a SPA (single-page application) from scratch.

We quickly implemented a simple backend service that exposes a GET /users endpoint.

Also, we implemented a frontend application that is trying to call this API.

Note that frontend and backend services are separate applications with different URIs. This is a typical case for SPAs.

Next, we want to test the flow. We launch our project and see the following error in the browser:

However, the backend responded correctly, status code 200 is proof of that:

Let’s start troubleshooting this problem by finding out what the term origin mentioned in the error message means.

What is Origin?

Origin is a combination of protocol (http), domain name (localhost) and port (3000 and 5008). In our case, the protocol and domain are the same, but the ports are different. Therefore, the origin is considered different.

Note that the query parameters and paths are ignored when it comes to origin comparison.

For example, the following pair of origins are considered to be the same:

This knowledge is important for understanding the behavior of CORS.

Same-Origin Policy

The problem arises because of the same-origin policy.

Same-origin policy requires that a resource must be loaded from the same origin as the client application that is trying to access it.

In our case, the origin of the frontend application is http://localhost:3000 and the origin of the backend service is http://localhost:5008.

Origins don’t match so that the browser throws an error.

This is just a security feature of a browser that was implemented to protect users from the Cross-Site Request Forgery attack.

✅ Here’s how this feature works in a successful scenario:

1. When the browser performs a cross-origin request, it adds an Origin header with the current origin (http://localhost:3000) to the request:

2. The server adds Access-Control-Allow-Origin to the response with the origin it wants to allow (also, http://localhost:3000).

3. The browser sees that Origin and Access-Control-Allow-Origin headers match and processes the response without any problems.

This behavior may depend on the type of request. For example, not all cross-origin requests are blocked by the browser (for example, requests which retrieve images or scripts). For such requests, the browser will not add an origin header. Another exception is preflight requesters. For destructive HTTP operations such as PUT, PATCH, DELETE, and some other cases, the browser initiates a handshake request to determine if the backend supports CORS before sending the actual request.

❌ But here’s what happens in our unfortunate case:

1. The browser is attaching the Origin header to the request:

2. The server does not add Access-Control-Allow-Origin header to the response at all (because we haven’t configured it yet):

As you can see, there is no Access-Control-Allow-Origin in the response headers.

3. The browser raises a CORS error because there is no expected response header.

⚠️ Note that CORS is only a feature of the browser. You can easily bypass it by calling the backend API from Postman or another HTTP client.

Now let’s see what can be done to solve the CORS problem in the case of SPA.

How to Configure CORS in ASP.NET Core?

Setting up CORS is a pretty straightforward process in ASP.NET Core.

To do this, we need to go to the Program file and create a CORS policy. Let’s start by defining a policy that will allow any origin:

builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAnyOrigin",
policy => policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
});

We defined a policy named AllowAnyOrigin that allows any origin.

Next we need to apply it. In the same Program file, we need to use the CORS middleware:

app.UseCors("AllowAnyOrigin");

If we run our SPA, the CORS error will disappear.

Let’s also check the Network tab for our /users request.

As a result of our changes to the Program file, the Access-Control-Allow-Origin header with an asterisk is returned by the backend. This header tells the browser which origins are allowed to access the response.

The asterisk allows any origin to use the /users API (and any other APIs the backend exposes).

So the browser is happy and no longer gives the CORS error.

However, such configuration opens up security issues. It should only be used for public APIs that meant to be used by any client.

More often than not, the backend only needs to allow a specific set of origins. To do so, we can refactor our policy as follows:

//Define a policy
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigins",
policy => policy.WithOrigins("http://localhost:3000/"));
});

//Add a CORS Middleware
app.UseCors("AllowSpecificOrigins");

However, if we run the application, we get the CORS error again.

Do you see what the problem is? Don’t worry if you don’t see it right away.

A while back I spent about half an hour researching this silly problem caused by the presence of an end slash in the URI.

The correct URI should look like this:

policy.WithOrigins("http://localhost:3000"));

Let’s check the response for the /users API on the Network tab again:

As a result of the policy change, the value for the Access-Control-Allow-Origin header was changed from * to http://localhost:3000, which is a much better option from a security perspective.

Move Allowed Origins to App Settings

At this point, we just hard-coded the allowed origin in the code.

While this is fine for local development, it obviously won’t work in higher environments like QA, UAT and so on.

To do this properly, we need to move the allowed origin to the app settings.

First we need to define a new section in the appsettings.json file:

{
//...

"AllowedOrigins": [ "http://localhost:3000" ]

//...
}

Note that we have defined the array so that it can be easily extended with new origins if needed.

Then we need to read the allowed origins from the appsettings.json in the Program file.

Here is one way to do this:

builder.Services.AddCors(options =>
{
var allowedOrigins = builder.Configuration
.GetSection("AllowedOrigins")
.Get<string[]>();

options.AddPolicy("AllowSpecificOrigins",
policy => policy.WithOrigins(allowedOrigins));
});

We have separated our code from the hardcoded origins. Once this code is merged into the main branch and deployed to higher environments, the correct array of allowed origins can be provided by the corresponding application configuration instance.

Conclusion

We covered what a CORS policy is, why it is needed, and how to configure CORS in ASP.NET Core applications.

We also figured out how to add CORS to the application configuration.

Thanks for reading. If you liked what you read, check out the story below. Also, please support me on Buy Me a Coffee and follow me on Patreon.

--

--