Cross-Domain requests and CORS
The Cross-Domain Issue
A HTTP request is said to be a cross-domain request when a resource makes that request to a different domain than it originated from. Web pages frequently make Cross-Domain requests to load images, scripts and CSS files. Cross-Domain requests that are fired from JavaScript have some restrictions according to the Same-Origin Policy. Normally browsers don't allow making such requests due to security reasons.
Frequently we face scenarios to consume third-party services, feeds etc. from JavaScript, one of the ways to achieve that is through JSONP calls. But the JSONP mechanism has its own limitations they are used to make only HTTP GET requests and they are vulnerable to security issues. Because of the desires of web community the W3C has come up with a new policy called Cross-Origin Resource Sharing or simply CORS that makes cross-domain requests so easy and natural.
This article discuss about the basic things of CORS. I had planned to include a real working sample but thinking about the size of the post I decided to write a separate article about that. Though this article sounds more theory but I think its good to know the basics behind this concept before going into practial implementations.
CORS
Because of the security limits imposed by the Same-Origin policy the browsers don't allows to make Cross-Domain requests from XmlHttpRequest object. Although there are workarounds and hacks that make this possible but nothing is perfect, to address this W3C has come up with this new policy CORS. Sometimes people also refer this in another named called Http Access Control.
A set of new HTTP headers have been introduced in CORS that both the server and the browser have to understand and act accordingly. One of the very important headers that make the cross-domain resource access itself possible is the Access-Control-Allow-Origin. The server has to purposely return this header in the response with the value either the domain name from where the request is originated or the wild card character *.
For example, if a request has been sent from the domain mydomain.com to the domain anotherdomain.com/current-news.php then the anotherdomain.com has to send a response with header Access-Control-Allow-Origin: mydomain.com, in simple words the anotherdomain.com is showing a green flag to mydomain.com for accessing its resource current-news.php. If the server returns "*" then from any domain one can access the anotherdomain.com's resource current-news.php. Most importantly if the server doesn't returns that header or returns some other domain name instead of the requesting domain then the browser should reject the response.
Example Code
var xhr = new XMLHttpRequest(); xhr.open("GET", "http://anotherdomain.com/current-news.php"); xhr.onreadystatechange = handler; xhr.send();
At the higher level the changes have to be done in both the server and the browser. The developers have to take care of handling and sending HTTP headers at the server-side but in the client-side browsers have to take the responsibility of adding and processing headers. Most modern browsers have already started supporting CORS but some still lags. The standard compliant browsers like Firefox, Chrome has extended their native XmlHttpRequest object for CORS support while IE has come up with a new object named XDomainRequest.
The following is the list of browsers that supports CORS (from wikipedia)
- Gecko 1.9.1 (Firefox 3.5, SeaMonkey 2.0) and above
- WebKit (Initial revision uncertain, Safari 4 and above, Google Chrome 3 and above... possibly earlier)
- MSHTML/Trident 4.0 (Internet Explorer 8) provides partial support via the XDomainRequest object.
Dive into CORS
Based upon the type of the request and other parameters there are two cases in CORS, Simple and Complex.
Simple Case
In the Simple Case there is only one request/response involved between the browser and the server. This case occurs when you make a GET or POST request without any custom HTTP headers. In POST request the Content-Type should not be other than application/x-www-form-urlencoded, multipart/form-data or text/plain. All the other cases like when you issue PUT or DELETE request, when you send some custom headers or when you send a different Content-Type than said above in POST they all fall in the Complex case.
Example Code
var xhr = new XMLHttpRequest(); xhr.open("GET", "http://anotherdomain.com/current-news.php"); xhr.onreadystatechange = handler; xhr.send();
In the above example we are issuing a GET request to http://anotherdomain.com/current-news.php, below are request and response headers.
Request / Response Headers
GET /current-news.php
Host: anotherdomain.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:6.0.2) Gecko/20100101 Firefox/6.0.2
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: http://mydomain.com/default.html
Origin: http://mydomain.com
HTTP/1.1 200 OK
Cache-Control: private
Content-Length: 2049
Content-Type: application/xml; charset=utf-8
Server: Microsoft-IIS/7.5
Access-Control-Allow-Origin: http://mydomain.com
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Sat, 12 Nov 2011 05:06:18 GMT
We can see some new headers in the request and response. The Origin header in the request gives the domain name from which the request has been made. We already saw about the Access-Control-Allow-Header, in this case it returns the domain name passed in the Origin header, it can also be "*". If the server doesn't return this header or the header has a different domain name then the browser should reject the response.
Complex Case
In the Complex Case more than one request/response is involved for accessing a cross-domain resource, normally the browser will sent an OPTIONS request prior to the actual request. The request that is sent initially is called as Preflight Request. The browser will sent a preflight request in any of these cases when the client issue a request other than GET or POST (ex. a DELETE request), when they try to send custom headers in the request or when they use a different MIME type like application/xml in the POST request.
The preflight request is issued basically to check whether the server can allow the actual request. In simply words the browser is just asking the server "Hey, I'm going to issue a DELETE request with a custom header, Do you allow that?" and the server will just reply either "No. I'll only allow GET or POST requests without any custom headers" or "Yes please". The browser will issue only the actual request if it has got a green signal.
Example Code
var xhr = new XMLHttpRequest(); xhr.open("DELETE", "http://anotherdomain.com/user/delete.php"); xhr.setRequestHeader('X-CUSTOM-HEADER', 'custom'); xhr.onreadystatechange = handler; xhr.send();
Preflight Request / Response Headers
OPTIONS /user/delete.php HTTP/1.1
Host: anotherdomain.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:6.0.2) Gecko/20100101 Firefox/6.0.2
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://mydomain.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: X-CUSTOM-HEADER
HTTP/1.1 200 OK
Server: Microsoft-IIS/7.5
Access-Control-Allow-Origin: http://mydomain.com
Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS
Access-Control-Allow-Headers: X-CUSTOM-HEADER
Access-Control-Max-Age: 3600
X-Powered-By: ASP.NET
Content-Length: 0
Date: Sat, 12 Nov 2011 05:08:20 GMT
Actual Request / Response Headers
DELETE /user/delete.php HTTP/1.1
Host: anotherdomain.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:6.0.2) Gecko/20100101 Firefox/6.0.2
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
X-CUSTOM-HEADER: custom
Content-Type: application/x-www-form-urlencoded
Referer: http://mydomain.com/default.html
Content-Length: 55
Origin: http://mydomain.com
HTTP/1.1 200 OK
Server: Microsoft-IIS/7.5
Access-Control-Allow-Origin: http://mydomain.com
X-Powered-By: ASP.NET
Content-Type: application/xml; charset=utf-8
Content-Length: 0
Date: Sat, 12 Nov 2011 05:08:23 GMT
In the above example the client is trying to issue a DELETE request with a custom header named X-CUSTOM-HEADER. Since it's not a Simple Case the browser makes a preflight request (OPTIONS request) to know whether the server allows DELETE request with that custom header. You can see two new HTTP headers other than the Origin, Access-Control-Request-Method and Access-Control-Request-Headers. The important thing is all these headers should be added by the browser not by the programmer.
When the server receives an OPTIONS request typically it will read the values from these headers and do some processing before sending a response or in the simple scenario it will give a response with the allowed request methods and headers. In our example server allows so you can find three more new HTTP headers in the response; Access-Control-Allow-Methods, Access-Control-Allow-Headers and Access-Control-Max-Age.
The Access-Control-Allow-Methods gives the allowed request methods in the server, in this case its GET, POST, DELETE and OPTIONS. The Access-Control-Allow-Headers gives the comma separated headers list that a client can send to the server. The third header Access-Control-Max-Age though sounds less important it says the browser the amount of time (3600 seconds = 1 hour) the response can be cached before sending another preflight request. So if the user sends another Complex case request before 1 hour the browser don't make a preflight request instead it read the results from the cache.
Since the actual request method (DELETE) and custom header (X-CUSTOM-HEADER) are allowed in the server the browser makes the DELETE request sending the custom header. If either the DELETE is absent in the Access-Control-Allow-Methods or custom header is missing in the Access-Control-Allow-Headers then the browser should not make the DELETE request.
We have pretty much covered CORS but there is one more scenario I would like to discuss and it’s about security.
Security in CORS
This is a special case and occurs when the client tries to send a cookie or basic authentication credentials in the request. The security comes into picture in both the Simple and Complex cases. According to the CORS specification when the client tries to send credentials the browser should reject the response if the server doesn’t return the header Access-Control-Allow-Credentials as true. In Complex case, if the response to preflight request doesn’t have the Access-Control-Allow-Credentials as true then the browser should not issue the actual request with credentials.
Example Code
var xhr = new XMLHttpRequest(); xhr.open("POST", "https://anotherdomain.com/login.php"); xhr.withCredentials = "true"; xhr.onreadystatechange = handler; xhr.send();
One important thing to note down in the above example is the withCredentials property of the XmlHttpRequest object, this is a new property that has been introduced as XMLHttpRequest Level 2 we have to specify it as true when we share credentials or cookies across domain.
Request / Response Headers
POST /login.php HTTP/1.1
Host: anotherdomain.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:6.0.2) Gecko/20100101 Firefox/6.0.2
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip, deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: http://mydomain.com /default.html
Origin: http://mydomain.com
Authorization: Basic dmlqYDFCG0aWNocmlzdA==
Cache-Control: max-age=0
HTTP/1.1 200 OK
Cache-Control: private
Content-Length: 20
Server: Microsoft-IIS/7.5
Access-Control-Allow-Origin: http://mydomain.com
Access-Control-Allow-Credentials: true
X-AspNet-Version:: 4.0.30319
X-Powered-By: ASP.NET
Date: Sat, 12 Nov 2011 08:37:15 GMT
CORS HTTP Headers
The below table list down all the request / response HTTP headers that have been introduced newly by W3C.
Name | Type | Description |
---|---|---|
Origin | Request | Indicates from where the cross-domain request originates from. Ex. Origin: http://mydomain.com |
Access-Control-Request-Method | Request | Indicates which method will be issued in the actual request as part of the preflight request. Ex. Access-Control-Request-Method: DELETE |
Access-Control-Request-Headers | Request | Indicates which headers will be used in the actual request as part of the preflight request. Ex. Access-Control-Allow-Headers: X-CUSTOM-HEADER |
Access-Control-Allow-Origin | Response | Indicates whether the request can be shared by returning the origin domain or "*" in the response. Ex. Access-Control-Allow-Origin: http://mydomain.com or "*" |
Access-Control-Allow-Methods | Response | Indicates the HTTP request methods that are allowed in the server. Ex. Access-Control-Allow-Methods: GET, OPTIONS, POST, DELETE |
Access-Control-Allow-Headers | Response | Indicates the comma separated HTTP headers that can be sent to the server. Ex. Access-Control-Allow-Headers: X-CUSTOM-HEADER, X-PING-HEADER |
Access-Control-Allow-Credentials | Response | Indicates whether the response to request can be exposed when the credentials flag is true.
In the case of preflight request it indicates whether the actual request can be made with the credentials. Ex. Access-Control-Allow-Credentials: true |
Access-Control-Max-Age | Response | Indicates how long the results of the preflight request can be cached before sending another one. Access-Control-Max-Age: 3600 |
Conclusion
Though by CORS we can't give a complete solution for the cross-domain issue that works in all the browsers at this time. But I'm sure the browsers that are lagging support are already in work. CORS is a standard solution for the cross-domain issue and its better than other implementations floating out there. So I recommend to go for it first and if it not works then try for JSONP or other strategies.