Archive for .NET

Implementing cross-browser CORS support for ASP.NET Web API

Browsers lock down cross-domain calls as a security measure against cross-site request forgery attacks, however, there are times that you legitimately need to make cross-domain requests to get data that your application needs.

CORS (Cross Origin Resource Sharing) defines a mechanism to allow such cross-domain requests to happen successfully between the client and the server.

Modern browsers have gotten significantly better at providing CORS support, but you still have to jump through hoops to make this work consistently across browsers. Approaches like using jQuery’s JSONP or explicitly setting $.support.cors = true have their shortcomings.

This post will cover the things you need to take care of in JavaScript client code and at the API level to properly handle cross-domain calls.

Cross-domain calls in an ASP.NET Web API scenario

Consider this simplified application architecture:

  • An ASP.NET MVC4 web application at http://app.mydomain.com
  • An ASP.NET MVC4 Web API application at http://api.mydomain.com
  • A SQL Server database containing your application’s data

In such an architecture, when a user interacts with your web application, the Web API acts as the middle man; sending data back and forth between the web application and the database.

You will also most likely host your API at a different URL or even a different server to maintain separation. In modern web applications, the application at http://app.mydomain.com (and other clients of your API) will use JavaScript to make calls to your API. These calls are cross-domain calls because the resource being called is outside the domain of the calling page.

Handling cross-domain calls on the client

Needing to support multiple browsers is a fact of life by now for developers, and once again, this is an IE vs. everyone else problem. To be fair, IE 10 finally gets it right. However, we have support IE 8 and IE 9 for some time to come.

Let’s assume that you’re using jQuery’s $.ajax() – which wraps XMLHttpRequest – to make calls to the API. You’ll quickly notice that this will cause IE to prevent the call from happening and/or display a security warning to the user.

To provide consistent cross-browser support for AJAX calls, you can write a generic JavaScript transport function that uses the XDomainRequest object for IE and encapsulates $.ajax() for all other callers.

Here’s a generic executeRequest function that accepts the following parameters:

  • The URL to call
  • The HTTP verb to use
  • Any data to include in the body of the request
  • A success callback
  • A failure callback
transport = function () {

    var verb = function () {
        var post = "POST";
        var get = "GET";
        var put = "PUT";
        var del = "DELETE";

        return {
            post: post,
            get: get,
            put: put,
            del: del
        };
    }();

    var executeRequest = function (
		method, 
		url, 
		data, 
		doneCallback, 
		failCallback) {

		var isCrossDomainRequest = url.indexOf('http://') == 0 
			|| url.indexOf('https://') == 0;

        if (window.XDomainRequest 
			&& $.browser.msie 
			&& $.browser.version < 10 
			&& isCrossDomainRequest) {

            // IE 10 fully supports XMLHttpRequest 
			//  but still contains XDomainRequest. 
            // Include check for IE < 10 to force IE 10 calls 
			//	to use standard XMLHttpRequest instead.
            // Only use XDomainRequest for cross-domain calls.

            var xdr = new XDomainRequest();
            if (xdr) {
                xdr.open(method, url);
                xdr.onload = function() {
                    var result = $.parseJSON(xdr.responseText);
                    if (result === null 
						|| typeof(result) === 'undefined') {
							result = $.parseJSON(
								data.firstChild.textContent);
                    }

                    if ($.isFunction(doneCallback)) {
                        doneCallback(result);
                    }
                };
                xdr.onerror = function() {
                    if ($.isFunction(failCallback)) {
                        failCallback();
                    }
                };
                xdr.onprogress = function() {};
                xdr.send(data);
            }
            return xdr;
        } else {
            var request = $.ajax({
                type: method,
                url: url,
                data: data,
                dataType: "JSON",
                contentType: 'application/json'
            }).done(function (result) {
                if($.isFunction(doneCallback)) {
                    doneCallback(result);
                }
            }).fail(function (jqXhr, textStatus) {
                if($.isFunction(failCallback)) {
                    failCallback(jqXhr, textStatus);
                }
            });

            return request;
        }
    };

    return {
        verb: verb,
        executeRequest: executeRequest
    };
}();

Note that I’m choosing to let IE 10 use jQuery’s $.ajax(). IE 10 still includes the XDomainRequest object but there’s no point in using it since the browser can now make cross-domain calls without it.

Here’s an example of calling the executeRequest function for a simple GET operation:

transport.executeRequest(
	transport.verb.get,
	url,
	null,
	function(result) {
		// do something with result data
	},
	function(jqXhr, textStatus) {
		// do something with failure data
	}

That’s all you have to do on the client side, let’s move on to implementing cross-domain support for the ASP.NET Web API.

Implementing a delegating handler for cross-domain requests

When a browser makes a cross-domain call, it will add an additional HTTP header to the request called Origin. The value in this header identifies the domain that the request is coming from.

Here’s a screenshot from Fiddler showing the Origin header in the request:

1

The API should respond to this request with an additional response header called Access-Control-Allow-Origin.

Here’s a screenshot from Fiddler showing the Access-Control-Allow-Origin header in the response.

CORS-2

It’s worth noting that the different browsers show different behavior as far as OPTIONS is concerned. I observed that Firefox and Chrome sent an OPTIONS request, but IE 9 did not. I haven’t tested with IE 10.

A browser will also send a preflight request to interrogate the API about its capabilities for cross-domain requests. This special type of request uses the OPTIONS HTTP verb.

In an OPTIONS request, the client sends a request with the following headers:

  • Access-Control-Request-Method
  • Access-Control-Request-Headers

and the server responds to the request with the following headers:

  • Access-Control-Allow-Methods
  • Access-Control-Allow-Headers

Here’s a screenshot from Fiddler showing the OPTIONS request.

CORS-3

Here’s a screenshot from Fiddler showing the response to the OPTIONS request.

CORS-4

To implement support for this on the API side, you have to write a Delegating Handler. The handler allows you to handle requests to the API and manipulate the response headers before the ASP.NET Web API engine processes the request.

Carlos Figueira does a great job in this MSDN article explaining how to write such a handler to implement CORS support for ASP.NET Web API. This code is based directly on Carlos’ article.

public class CorsHandler : DelegatingHandler
{
	private const string AccessControlAllowHeaders 
		= "Access-Control-Allow-Headers";
	private const string AccessControlAllowMethods 
		= "Access-Control-Allow-Methods";
	private const string AccessControlAllowOrigin 
		= "Access-Control-Allow-Origin";
	private const string AccessControlRequestHeaders 
		= "Access-Control-Request-Headers";
	private const string AccessControlRequestMethod 
		= "Access-Control-Request-Method";
	private const string Origin = "Origin";

	protected override Task<HttpResponseMessage> SendAsync(
		HttpRequestMessage request, 
		CancellationToken cancellationToken)
	{
		var isCorsRequest = request.Headers.Contains(Origin);
		var isPreflightRequest = request.Method == HttpMethod.Options;

		if (isCorsRequest)
		{
			if (isPreflightRequest)
			{
				var response = new HttpResponseMessage(HttpStatusCode.OK);

				response.Headers.Add(
					AccessControlAllowOrigin, 
					request.Headers.GetValues(Origin).First());

				var accessControlRequestMethod = request.Headers.GetValues
					AccessControlRequestMethod).FirstOrDefault();

				if (accessControlRequestMethod != null)
				{
					response.Headers.Add(
						AccessControlAllowMethods, 
						accessControlRequestMethod);
				}

				var requestedHeaders = String.Join(", ", 
					request.Headers.GetValues(AccessControlRequestHeaders));

				if (!string.IsNullOrEmpty(requestedHeaders))
				{
					response.Headers.Add(
						AccessControlAllowHeaders, 
						requestedHeaders);
				}

				var tcs = new TaskCompletionSource<HttpResponseMessage>();
				tcs.SetResult(response);
				return tcs.Task;
			}
			else
			{
				return base.SendAsync(
					request, 
					cancellationToken)
					.ContinueWith<HttpResponseMessage>(t =>
				{
					var resp = t.Result;
					resp.Headers.Add(
						AccessControlAllowOrigin, 
						request.Headers.GetValues(Origin).First());
					return resp;
				});
			}
		}
		else
		{
			return base.SendAsync(request, cancellationToken);
		}
	}
}
Implementing a delegating handler for missing content type header when using XDomainRequest

A side-effect of using the XDomainRequest object for requests coming from IE is that it doesn’t allow you to manipulate the request headers.

This is critical because when creating an XMLHttpRequest, you may need to set the value of the content-type header to application/json if your request contains JSON.

The solution to this is to write another Delegating Handler. In this case, the handler inspects the request to see if its content-type header is present. If it is not, it adds it and sets its value to application/json before passing it along to the ASP.NET Web API engine.

Here’s the code for this handler:

public class ContentTypeHandler : DelegatingHandler
{
	protected override Task<HttpResponseMessage> SendAsync(
		HttpRequestMessage request, 
		CancellationToken cancellationToken)
	{
		if (request.Method == HttpMethod.Post 
			&& request.Content.Headers.ContentType == null)
		{
			request.Content.Headers.ContentType 
				= new MediaTypeHeaderValue("application/json");
		}

		return base.SendAsync(request, cancellationToken);
	}
}
Conclusion

It’s a pity that you have to jump through so many hoops to get true cross-browser CORS support working. I’m hoping this becomes more of a configuration/deployment issue than a development one.

There’s a small shortcoming in the JavaScript executeRequest function that I’d like to call out. The non-XDomainRequest section of the code supports JavaScript promises, allowing you to use jQuery functionality such as $.when() to wait until a set of asynchronous operations have completed. I’m still working on getting this working properly for IE by manually creating the jQuery Deferred object.