How to enable CORS for POST requests on a single endpoint in ASP.NET Core
asp.net-core

How to enable CORS for POST requests on a single endpoint in ASP.NET Core

This article explains a way to enable CORS requests for a single endpoint in ASP.NET Core, without using the official middleware. It has been made to add flexibility to the way CORS is handled.

Today I looked for a way to enable CORS requests for a specific endpoint on a different subdomain. You may reach some cases where you would like to enable CORS on a single URL, this article explains how to manually implement it. There may be some other ways using the framework, but sometimes I like to have flexible behaviors that I understand from A to Z.

What is CORS - Cross Origin Resource Sharing

You may have reached some issues such as

Access to font at 'https://sub1.example.com/lib/webfonts/font.ttf' from origin 'https://sub2.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

or

Access to XMLHttpRequest at 'https://sub2.example.com/find' from origin 'https://sub1.example.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

or

Cross-Origin Read Blocking (CORB) blocked cross-origin response https://sub2.example.com/find with MIME type application/json. See https://www.chromestatus.com/feature/5629709824032768 for more details.

These are a bit frustrating when you are not familiar with CORS constraints. For several security reasons, some policies have been introduced to the http protocol.

When calling http methods that may impact the server on cross domain (POST, PATCH, PUT etc), modern browsers begin with a preflight request, the flow looks like:

sub1 --> sub2 : OPTIONS https://sub2.example.com/find
sub2 --> sub1 : Ok
sub1 --> sub2 : POST https://sub2.example.com/find
sub2 --> sub1 : Response

Our objective will be to implement this flow and ensure it is acceptable on client-side.

Why do CORS requests fail on default implementations?

If you do routing based on Http verbs, you may be doing something like this:

[HttpPost("/find", Name="Find_Route")]
public async Task FindOptions([FromForm]Find_POSTModel model)
{
}

The OPTIONS call mentioned above will never hit this endpoint and the call will fail for two reasons:

  • It is restricted to POST calls
  • It requires a model that is not sent with the OPTIONS request

The following sections will help you to go through this issue and better understand what it implies.

If you want more information about CORS, you can find a good description of CORS on Mozilla website, you may also find an official way of using CORS in ASP.NET Core on Microsoft website, that uses built-in middlewares and filters.

Preliminary work

We will be using few methods in our controller, the following one will be used to identify the site that is sending the request.

Uri GetOrigin()
{
    Uri origin = null;
    var originHeader = Request.Headers["Origin"].FirstOrDefault();
    if (!String.IsNullOrEmpty(originHeader)  && Uri.TryCreate(originHeader, UriKind.Absolute, out origin))
        return origin;
    return null;
}

The next method will be used to determine which external websites are allowed to send cross-domain request. For the example simplicity, the policy will be hardcoded, but it could be made dynamic very easily by making this method asynchronous to handle file-sourced filters or database requests.

bool IsOriginAllowed(Uri origin)
{
    const string myDomain = "mydomain.com";
    const string[] allowedDomains = new []{ "example.com", "sub.example.com" };

    return 
           // allow from a list of domains
           allowedDomains.Contains(origin.Host) 
           // allow any sub-domain
           || origin.Host.EndsWith($".{myDomain}");
}

Manual handling of the 'OPTIONS' method

Now we are ready to begin with the handling of our incoming requests. We will start by handling the OPTIONS method calls. In short, it would be done by following 3 steps:

  • Get the calling origin site
  • Check whether it is authorized or not to perform the call (of course it should not be considered as a security check)
  • Set the relevant headers and send the Ok answer (or bad)
[HttpOptions("/find")]
public IActionResult FindOptions()
{
    // Get the origin header as Uri
    var origin = GetOrigin();
    // Check whether the caller is allowed or not
    var isAllowed = IsOriginAllowed(origin);
    if(isAllowed)
    {
        Response.Headers.Add("Access-Control-Allow-Origin", new[] { (string)Request.Headers["Origin"] });
        Response.Headers.Add("Access-Control-Allow-Headers", new[] { "Origin, X-Requested-With, Content-Type, Accept" });
        Response.Headers.Add("Access-Control-Allow-Methods", new[] { "POST, OPTIONS" }); // new[] { "GET, POST, PUT, DELETE, OPTIONS" }
        Response.Headers.Add("Access-Control-Allow-Credentials", new[] { "true" });
        return NoContent();
    }
    // return an error status code
    return BadRequest(); 
}

We could return forbidden, but I am not a fan of giving too much information to unwanted requests.

Adjustments to the POST response to allow CORS

The last step consists on adding a CORS header to the POST request that is already implemented. We can implement this in a separated method that will update the Response object.

[HttpPost("/find")]
public async Task<IActionResult> FindOptions([FromForm]Find_POSTModel model)
{
    // Lets add everything CORS-related in a single method
    AllowCrossOrigin();
    
    // usual handling...
}
private void AllowCrossOrigin()
{
    Uri origin = GetOrigin();

    if (origin != null && IsOriginAllowed(origin))
        // If the origin is allowed, add the specific header to the response
        Response.Headers.Add("Access-Control-Allow-Origin", $"{origin.Scheme}://{origin.Host}");
}

We have seen how to handle manually POST requests on cross domain requests with ASP.NET Core in 3 steps: implementing a CORS policy for external domains, implementing the OPTIONS method and adjusting the response headers of the existing POST handler.


Send
Please sign-in to comment
.X0001-01-01_00-00