CORS and WebAPI
Introduction
CORS (Cross-origin resource sharing) is a way in which a browser can make a request to a web server other then the one which served up the original resource.
The CORS specification mandates that requests that use methods other than POST or GET, or that use custom headers, or request bodies other than text/plain, are preflighted. A preflighted request first sends the OPTIONS header to the resource on the other domain, to check and see if the actual request is safe to send. This capability is currently not supported by IE8′s XDomainRequest object, but is supported by Firefox 3.5 and Safari 4 with XMLHttpRequest. The web developer does not need to worry about the mechanics of preflighting, since the implementation handles that.
You can achieve this using a DelegatingHandler in ASP.Net Web API - the way it works is to:
- Identify requests containing the "Origin" header"
- If the request is NOT of HTTP method "OPTIONS" (no preflighting) then we add the following headers to the response:
- Access-Control-Allow-Origin - this will have the same value as the Origin value passed in the request.
- Access-Control-Allow-Credentials - this will have the value true, allowing requests to contain basic-auth credentials
- If the request is of HTTP method "OPTIONS", then we treat this as a "pre-flighting" request, responding with a 200-OK response, and returning the following headers in the response:
- Access-Control-Allow-Origin - this will have the same value as the Origin value passed in the request header.
- Access-Control-Allow-Credentials - this will have the value true, allowing requests to contain basic-auth credentials
- Access-Control-Allow-Methods - this will have the same value as the Access-Control-Request-Method value passed in the request header.
- Access-Control-Allow-Headers - this will have the same value as the Access-Control-Request-Headers value passed in the request header.
And here's the code to achieve that:
public class CORSHandler : DelegatingHandler
{
const string Origin = "Origin";
const string AccessControlRequestMethod = "Access-Control-Request-Method";
const string AccessControlRequestHeaders = "Access-Control-Request-Headers";
const string AccessControlAllowOrigin = "Access-Control-Allow-Origin";
const string AccessControlAllowMethods = "Access-Control-Allow-Methods";
const string AccessControlAllowHeaders = "Access-Control-Allow-Headers";
const string AccessControlAllowCredentials = "Access-Control-Allow-Credentials";protected override Task
SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
bool isCorsRequest = request.Headers.Contains(Origin);
bool isPreflightRequest = request.Method == HttpMethod.Options;
if (isCorsRequest)
{
if (isPreflightRequest)
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
response.Headers.Add(AccessControlAllowOrigin,
request.Headers.GetValues(Origin).First());string accessControlRequestMethod =
request.Headers.GetValues(AccessControlRequestMethod).FirstOrDefault();if (accessControlRequestMethod != null)
{
response.Headers.Add(AccessControlAllowMethods, accessControlRequestMethod);
}string requestedHeaders = string.Join(", ",
request.Headers.GetValues(AccessControlRequestHeaders));if (!string.IsNullOrEmpty(requestedHeaders))
{
response.Headers.Add(AccessControlAllowHeaders, requestedHeaders);
}response.Headers.Add(AccessControlAllowCredentials, "true");
TaskCompletionSource
tcs = new TaskCompletionSource ();
tcs.SetResult(response);
return tcs.Task;
}
else
{
return base.SendAsync(request, cancellationToken).ContinueWith(t =>
{
HttpResponseMessage resp = t.Result;
resp.Headers.Add(AccessControlAllowOrigin, request.Headers.GetValues(Origin).First());
resp.Headers.Add(AccessControlAllowCredentials, "true");
return resp;
});
}
}
else
{
return base.SendAsync(request, cancellationToken);
}
}
}
If using basic-auth, it's worth noting that the pre-flighted request is unauthenticated - so add the CORSHandler to your configurations set of MessageHandlers prior to any authentication handlers, so the OPTIONS requests can be handled correctly.
Access-Control-Allow-Credentials
In the handler above we enable Access-Control-Allow-Credentials - meaning if the Web API supports basic auth, then the browser is able to authenticate with the API using basic-auth cross-origin, in a jQuery ajax request that would look like this:
$.ajax({
url: "http://localhost/myapp/api/collectionres,
type: "GET",
username: "mylogin",
password: "mypassword",
data: "$top=10"
xhrFields: {
withCredentials: true
},
crossDomain: true,
success: success
});
I definitely don't recommend this approach for production code - but, it can make for great way for people to play with an API programmatically in javascript, without having to resort to using Node (and using a tool like Dropbox allows them to privately "host" their html/javascript as well, making it possible to share and collaborate on simple mashups).