Introduction
In this tip, I am going to explain the basics of CORS, when it is used, and how you can manage it in any ASP.NET Web API project in a simple and elegant way.
What is CORS?
In today's application programming, it is becoming increasingly important to have a clear separation between the client/server logic. This is attributed to the popularity of mobile apps, CDNs, cloud computing and other web based microservices. This doesn't come without its own flaws. With the intention of sharing resources over HTTP, we open our systems to security risks. For the same reason, browsers have been built to follow Same-Origin
Policy
, which means that if you have a script attempting to fetch data from a domain other than where the resource resides, the browser would not allow such request to proceed. This is understandable, but it surely restricts an application from making cross-origin requests.
Historically, one would require to send a JSONP callback as a query parameter along with the request, which the server would then use to wrap the response in and send back to be processed at the client. This approach is a mere trick and suffers with XSS vulnerability, not to forget that it is cumbersome to manage if you are dealing with lots of resources and requests. The question is how do you allow your users to connect to your resource, which is provided over a Web API? The answer is CORS
. The term stands for Cross-Origin Resource Sharing and it lays down a standard for requesting any cross origin data from a remote server. What this effectively means is that any request accessing a resource on the server via HTTP methods (GET
, POST
, PUT
, DELETE
, PATCH
, etc.) must be allowed/disallowed by the server at the resource level itself.
In the CORS world, the user agent (say, a browser) is responsible for detecting if the outgoing request is simple or complex in nature, based on which appropriate actions are taken. A request is considered simple if one or a combination of following criterias are met:
- Method is
GET
, HEAD
or POST
in the request
- Header is
Accept
, Accept-Language
, Content-Language
and Content-Type
(only application/x-www-form-urlencoded
, multipart/form-data
, text/plain
are acceptable) in the request
If this is so, the user agent directly passes the cross-origin request to the server using Origin
(origin domain) header and if the origin is allowed by the server, it sends back a single header namely Access-Control-Allow-Origin
with the rest of the response.
All other combinations of methods and headers are deemed complex and the user agent must send a pre-flight request (OPTIONS
) before the intended cross-origin request, with below three headers as applicable:
Origin
- origin domain (http://example.com)
Access-Control-Request-Method
- HTTP method in the request
Access-Control-Request-Headers
- HTTP headers in the request
On receiving this pre-flight request, the server needs to check resource CORS configuration and can send the following headers in response based on matches it found.
Access-Control-Allow-Origin -
origin domain (http://example.com)
Access-Control-Request-Methods -
allowed HTTP methods
Access-Control-Request-Headers -
allowed HTTP headers
The server can also send additional headers (CORS or otherwise) with the response which the client may require to complete the request.
This is the most basic setup that is required on the server which should suffice for most present applications. However, the CORS spec is broader and includes cases for HTTP Cookie, Cache, among other things.
This tip will focus on a way to manage CORS by using "Frame.CORS
" nuget package, which is henceforth referred to as FRAME
.
Design Goals
It is fairly common that your application deals in multiple resources and you intend to provide access to them over HTTP by implementing a Web API. It is also desirable to configure CORS rules which allow cross-origin resource access. Mentioned below are few design goals that FRAME
library intends to accomplish:
- Ease of use (working with known formats and requiring minimal user intervention)
- Complete and granular (implements entire CORS spec and is highly configurable)
- Default and resource specific behaviours (ability to add server-wide defaults and override them for a specific resource or controller managing the resource)
- Logic separation (helps in code maintenance, code migration and configuration sharing)
- Local testing of the CORS-enabled API
Installing FRAME
FRAME
is available as a nuget package - Frame.CORS
. It supports .NET Framework 4.5 and above (till 4.6.2 as of this writing). You can download it from this link - https://www.nuget.org/packages/Frame.Cors/ or search "Frame.CORS
" from Nuget Package Manager.
Using FRAME
After the installation is finished, you will find a new file created in App_Data folder of your application - frm-cors-config.json. This is a CORS configuration file, and has a JSON array structure like this:
[{
"controllers": "*",
"allow-origin": "*",
"allow-methods": "GET,POST",
"allow-headers": "*",
"allow-credentials": false,
"max-age": 0,
"expose-headers": "*"
},{
"controllers": "home",
"allow-origin": "*",
"allow-methods": "*",
"allow-headers": "*",
"allow-credentials": false,
"max-age": 0,
"expose-headers": "*"
}]
Note
: If this file is not created due to permission issues or similar, please create it manually and insert the above JSON or download the attached file and keep it in App_Data folder.
Consider each JSON element in the array as a set of CORS config for controllers mentioned in the "controllers
" key of the same element. This and other key-value pairs are explained below:
controllers
: (string
) Specify controller names (without Controller suffix, like product) or * for all controllers. All the below described fields will apply to this set of controllers.
allow-origin
: (string
) Specify a single origin domain (including scheme, host and port, if applicable, like http://example.com) or * for all origins
allow-methods
: (string
) Specify access methods allowed in the request
allow-headers
: (string
) Specify access headers allowed in the request
allow-credentials
: (boolean
) Specify if credentials are allowed in the request
max-age
: (long
) Specify max-age in seconds that the response can be cached for by the user agent
expose-headers
: (string
) Specify which headers are to be exposed to the client in the response
If let's say, there are three controllers (ctrl1
, ctrl2
, ctrl3
) with ctrl1
and ctrl2
having same CORS set, the JSON would look like (example values):
[{
"controllers": "*",
"allow-origin": "*",
"allow-methods": "GET,POST",
"allow-headers": "*",
"allow-credentials": false,
"max-age": 0,
"expose-headers": "*"
},{
"controllers": "ctrl1,ctrl2",
"allow-origin": "http://example.com",
"allow-methods": "GET,POST,PUT,DELETE",
"allow-headers": "Authorization,Content-Type",
"allow-credentials": true,
"max-age": 300,
"expose-headers": "*"
},{
"controllers": "ctrl3",
"allow-origin": "http://example.com",
"allow-methods": "GET,POST",
"allow-headers": "Cache-Control,Content-Type"
}]
After the configuration is final, you simply need to import Frame.CORS
namespace reference in App_Start/WebApiConfig.cs file and register FRAME
with this piece of code:
config.RegisterFrame([bool runLocalOnly]);
The runLocalOnly
flag is optional (default = false
). Set it to true
only if the API is idle and is being currently used for testing purposes.
Important Points
There are a few important points to note with respect to using the library:
- As shown in the file above, you can specify a default set of headers for all controllers using "
*
" as value and then override them by adding another JSON object for a particular controller. Here, the entire new set overrides the default set and not individual fields. So, if a field like "expose-headers
" is to be removed from response, then delete it from the controller's set.
- You can omit the default set completely and go on to set individual JSON config for all or a subset of controllers.
- Multiple controllers can be grouped together in a comma-separated manner and that set will be applicable to all of them.
- Duplicate controller sets are not allowed. This means that there cannot be two JSON elements having
controllers
field with same controller name in them. The library will throw a runtime error in such a scenario.
- The library allows you to test the CORS functionality locally with the use of a flag (described below). When this is the case, CORS can be assumed to be turned off to the outer world and no cross origin requests are allowed to the server.
Note: Please make sure that the config file name and path are not modified after installation.