I needed something very simple:

A CORs policy that works with localhost on any port as well as some specific URLs for production. Simple? Not so much.

What is CORS?

Cross Origin Resource Sharing is a mechanism that is designed to stop people from using your API data sources on their web sites. How can you lock down an endpoint that needs to be public, by definition? Adding a username and password is no use because your web app would need those creds in the browser and they would be available for an attacker.

A time-based token like a JWT would be OK but would add quite a lot of complexity to do something that sounds like it should be much more simple.

CORS is something that browsers agree to do on behalf of the owner of the API so that if the API doesn't want to be called from a site other than www.example.com then the browser will not permit the reading of data from a call to the API in question. Note that the call will still be made but the browser will not let a script read the response.

Firstly, this is not a full-on security method and provides no privacy. Why? Because if you call the endpoint directly in a browser, CORs is not in-place - the origin is the same as the API endpoint so no "cross origin" request is being made - you can see the response in the browser.

What's the problem?

The method is quite subtle and provides some details which can make it hard to test and debug. Also, the functionality has changed between dotnet core 2 and 3 (maybe 2.1 and 2.2, not sure) and there are comments around the web which are out of date, too opinionated or plain wrong! If something's not working, it is hard to work out why.

The best thing is that the implementation is so simple in dotnet core that it shouldn't take too long to add things in step-by-step to find out what is wrong.

The code

Literally, all you need in Startup.cs (assuming you want the entire API locked down) is:

 public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  

app.UseEndpoints(endpoints => {
(which includes some of the more common options - UseCors must be before UseEndpoints or UseMvc by which point your Action will be hit instead of the Cors handler!)

And then something like:

 public void ConfigureServices(IServiceCollection services)  
services.AddCors(c =>
c.AddPolicy("AllowOrigin", options => options

(obviously with all the other services not shown here)

So hopefully you can initially see that the configuration is so lax (AllowAnyOrigin()) that this is a largely pointless exercise, unless this policy was only for a subset of actions. What we really want to do is to specify which origins we want to allow.

Specifying Allowed Origins

 services.AddCors(c =>  
c.AddPolicy("AllowOrigin", options => options
.WithOrigins("http://www.example.com", "https://www.example.com"));

But here we must pause and understand what makes the same origin and what is cross origin. https and http are two separate schemes. Although they are likely to be under the control of the same person, this is not necessarily guaranteed so they are considered separate origins and need specifying separately!

Likewise, different port numbers are not guaranteed to be under the control of the same people so different ports need to be specified. Hmm.

Using Localhost

Localhost is kind of special even though it shouldn't be. Browsers have treated this differently so that, for example, Chrome used to treat all localhost ports as the same origin even though it breaks the strict definition of same-origin. For our purposes we need to consider localhost to be the same as any other host and if we need to access our API from random localhost ports, we need to code for this. How can we do this in a string list? Not very easily!

We can add another extension method that allows us to lambda our origin matching, this is much more useful because we can use any string methods we want:

 services.AddCors(c =>  
c.AddPolicy("AllowOrigin", options => options
.SetIsOriginAllowed(origin => {
var hostName = new Uri(origin).Host;
return hostName == "localhost" || hostName.EndsWith("example.co.uk") || hostName.EndsWith("example.io");

In my example, we can ignore the port for localhost and allow subdomain matching on example.co.uk and example.io!

Testing it!

This is fiddly so if you don't have Postman installed and setup, do it now. It takes a while to setup environments and folders and stuff but once you start dealing with auth tokens and the like, it will save ages when you are trying things out!

Remember that CORs is an agreement, it is not enforce magically. In other words, if I simply call the GET endpoint for my API with the correct parameters, it will behave EXACTLY as if CORs was not implemented at all - which can be confusing. Why? Because this was made to be backwards compatible with browsers and APIs and if your front-end app was calling your backend api on the same domain, there is no need to check whether the call is allowed. It should be allowed and will be called and will work exactly as before!

So how do we test it? We have to behave the same way as the browser agrees to behave. We need to pre-flight the request if it is not considered a simple request. Say what?

A Simple CORs request

A simple request does NOT need the pre-flight check from the browser. A simple request is undemanding on resources and can be directly attempted by the browser. These requests limit the headers that are permitted and certain other constraints on the request (see here). Once the call is made, if it is simple, the response will be returned WITH an additional header: Access-Control-Allow-Origin.

If the request is cross origin but the origin host is matched by the returned header, the browser will allow it. If the request is cross origin and the header is not returned at all, the browser will assume it is not permitted and will not allow the page to read the response (but the call is still made so it won't stop DoS!)

A non-simple request

A non-simple request is anything that is not a simple request! These are defined as ones that might have side-effects if they were called before checking whether they are allowed. I don't know the details of how this is worked out but basically, the browser needs to ask for permission!

The browser will send an OPTIONS request to the endpoint it wants to call and the only thing it needs to send is an Origin header which is the host of the web application that is running.

The response is a 204 (no content) which either will contain Access-Control-Allowed-Origin either with a * or with the value passed as Origin if  it does not allow all origins. It will not return all origins that are permitted if you do not permit all origins so you cannot sniff headers that you don't already know. This response means it was successful.

If it is not successful, you still get a 204 no content but the header will be missing and the browser will know not to send the actual request.

Additional Headers

As well as Origin which must be present if sending a cross-origin request, there are two other headers that are usually sent with the pre-flight request.

The first is mandatory: Access-Control-Request-Method which matches the verb you are going to use for the endpoint. This allows the API to restrict some verbs different than others.

The second is optional but any headers that are not part of a standard request (such as Authorization) need to be passed in via Access-Control-Request-Headers. This again allows the API to make a CORs decision based on headers. For example, it might only allow calls that are going to pass Authorization and this allows some reduction of invalid calls from reaching your API.

Caching Preflight Checks

If you test it in production, you might be annoyed that each API call has a matching pre flight check. This is because the preflight checks are simply HTTP calls and follow the normal HTTP caching rules i.e. not cached by default!

In real life, most of our allowed origins are not going to change very often so we might want to cache these options responses that are generated by the Cors middleware in dotnet core. Fortunately, they provide a simple method to make this happen when configuring your Cors policy: