How to enable CORS for POST requests on a single endpoint in ASP.NET Core
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.