WCF + Cross Domain Ajax Calls (CORS) + Authorization

Good afternoon!
I would like to demonstrate one of the possible approaches to solving the problem of working with WCF services from various domains. The information I found on this topic was either incomplete or contained an excessive amount of information that made it difficult to understand. I want to talk about several ways of interaction between WCF and AJAX POST requests, including information about cookies and authorization.

As you know, just like that AJAX call to another domain will not work, due to security reasons. To solve this problem, the CORS standard was invented and released ( wiki , mozilla) This standard implies the use of specific HTTP headers to allow and restrict access. A simplified communication process using this protocol implies the following:

The client (browser) initiates a connection with an HTTP header Origin, the server must respond using the header Access-Control-Allow-Origin. An example of a request / response pair from an address foo.exampleto a service bar.other/resources/public-data:
Request:
GET / resources / public-data / HTTP / 1.1
Host: bar.other
Origin: foo.example
[Other headers]

Reply:
HTTP / 1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Access-Control-Allow- Origin: *
Content-Type: application / xml

[XML Data]


Headings

  • Access-Control-Allow-Origin- This heading determines from which resources requests may come. A *specific domain can be used , for example foo.example. This header can be only one, and can contain only one value, i.e. domain list cannot be specified.
  • Access-Control-Allow-Methods- This header defines which methods can be used to communicate with the server. We restrict ourselves to the following: POST,GET,OPTIONSbut you can also use both PUT, and DELETE, and others.
  • Access-Control-Allow-Headers- This title defines a list of available titles. For example Content-Type, which allows you to set the type of response application/json.
  • Access-Control-Allow-Credentials- This header determines whether cookie and authorization headers are allowed. Possible values true and false. Important: data will be transmitted only if Access-Control-Allow-Origina specific domain is explicitly set in the header , if used *, the header will be ignored and no data will be transmitted.

In general, the browser imposes restrictions. If he doesn’t like something in the headers, he will not give this data to the user (if the necessary one does not return Access-Control-Allow-Headers, or to the server if the Access-Control-Allow-Credentialscorrect one is not specified Access-Control-Allow-Origin. Before POSTrequesting to another domain, the browser will first make a OPTIONSrequest ( preflight request ) for information about permitted methods of working with the service.

WCF

There is a certain amount of information of varying quality on this topic. Unfortunately, WCF does not allow the use of these headers by standard means, but there are several solutions to this problem. I bring to your attention some of them.

Solution using web.config.

This solution involves adding the necessary headers directly to web.config.

It is distinguished by its simplicity and rigidity. In particular, this particular example cannot be used if there are more than one possible domain, in addition, it allows CORS to the entire site (in a particular case).

Solution Using Global.asax

This solution involves writing code in Global.asax.cs that adds the necessary headers to each request.
protected void Application_BeginRequest(object sender, EventArgs e) {
	var allowedOrigins = new [] { "http://foo.example", "http://bar.example" };
	var request = HttpContext.Current.Request;
	var response = HttpContext.Current.Response;
	var origin = request.Headers["Origin"];
	if (origin != null && allowedOrigins.Any(x => x == origin)) {
		response.AddHeader("Access-Control-Allow-Origin", origin);
		response.AddHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
		response.AddHeader("Access-Control-Allow-Headers", "Content-Type, X-Requested-With");
		response.AddHeader("Access-Control-Allow-Credentials", "true");
		if (request.HttpMethod == "OPTIONS") {
			response.End();
		}
	}
}

This solution supports multiple domains, but applies to the entire site. Of course, all the conditions for specific services can be set right there, but in my opinion this is associated with inconveniences in maintaining a list of allowed services.

Solution with adding headers in the WCF service code

This solution differs from the previous one only in that headers are added for a particular service or method. In general, the solution looks like this:
[ServiceContract]
public class MyService { 
    [OperationContract]
    [WebInvoke(Method = "POST", ...)]
    public string DoStuff() {
         AddCorsHeaders();
         return "";
     }
    private void AddCorsHeaders() {
        var allowedOrigins = new [] { "http://foo.example", "http://bar.example" };
        var request = WebOperationContext.Current.IncomingRequest;
        var response = WebOperationContext.Current.OutgoingResponse;
        var origin = request.Headers["Origin"];
        if (origin != null && allowedOrigins.Any(x => x == origin)) {
            response.AddHeader("Access-Control-Allow-Origin", origin);
            response.AddHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
            response.AddHeader("Access-Control-Allow-Headers", "Content-Type, X-Requested-With");
            response.AddHeader("Access-Control-Allow-Credentials", "true");
            if (request.HttpMethod == "OPTIONS") {
                response.End();
            }
        }
    }
}

This approach allows you to limit the use of CORS within a service or even a method. The main disadvantage is that a call is AddCorsHeadersrequired in every service method. Plus - ease of use.

Solution Using Native EndPointBehavior and DispatchMessageInspector

This approach uses the capabilities of WCF to expand the functionality.
2 classes are created EnableCorsBehavior:
using System;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
namespace My.Web.Cors {
    public class EnableCorsBehavior : BehaviorExtensionElement, IEndpointBehavior {
        public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { }
        public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { }
        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) {
            endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new EnableCorsMessageInspector());
        }
        public void Validate(ServiceEndpoint endpoint) { }
        public override Type BehaviorType {
            get { return typeof(EnableCorsBehavior); }
        }
        protected override object CreateBehavior() {
            return new EnableCorsBehavior();
        }
    }
}

and EnableCorsMessageInspector:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
namespace My.Web.Cors {
    public class EnableCorsMessageInspector : IDispatchMessageInspector {
        public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext) {
		    var allowedOrigins = new [] { "http://foo.example", "http://bar.example" };
            var httpProp = (HttpRequestMessageProperty)request.Properties[HttpRequestMessageProperty.Name];
            if (httpProp != null) {
                string origin = httpProp.Headers["Origin"];
                if (origin != null && allowedOrigins.Any(x => x == origin)) {
                  return origin;
                }
            }
            return null;
        }
        public void BeforeSendReply(ref Message reply, object correlationState) {
            string origin = correlationState as string;
            if (origin != null) {
                HttpResponseMessageProperty httpProp = null;
                if (reply.Properties.ContainsKey(HttpResponseMessageProperty.Name)) {
                  httpProp = (HttpResponseMessageProperty)reply.Properties[HttpResponseMessageProperty.Name];
                } else {
                  httpProp = new HttpResponseMessageProperty();
                  reply.Properties.Add(HttpResponseMessageProperty.Name, httpProp);
                }
                httpProp.Headers.Add("Access-Control-Allow-Origin", origin);
                httpProp.Headers.Add("Access-Control-Allow-Credentials", "true");
                httpProp.Headers.Add("Access-Control-Request-Method", "POST,GET,OPTIONS");
                httpProp.Headers.Add("Access-Control-Allow-Headers", "X-Requested-With,Content-Type");
            }
        }
    }
}

Add to the web.configcreated EnableCorsBehavior:

...
	
...

Find and add the EnableCorsBehavior extension created for the configuration of Behaviorour Endpoint'a

	...
	
		...
		
			...
			
			...
		
		...
	
	...

We only need to process the preliminary request with the method OPTIONS. In my case, I used the simplest option: a OPTIONS request handler method is added to the body of the service .
[OperationContract]
[WebInvoke(Method = "OPTIONS", UriTemplate = "*")]
public void GetOptions()
{
    // Заголовки обработаются в EnableCorsMessageInspector 
}

Of course, there is a similar WCF extension for working with preflight requests, one of them can be read by reference from the list of references at the end of the article. The main disadvantage is the need to add a method GetOptionsto the body of the service and a considerable amount of additional code. On the other hand, this approach allows us to almost completely separate the logic of service and the logic of communication.

A few words about Javascript and browsers

To support sending authorization and cookie data, it is necessary that the XmlHttpRequest be set to a trueflag withCredentials. I think that many people use jQuery to work with AJAX, so I will give an example for it:
 $.ajax({
            type: 'POST',
            cache: false,
            dataType: 'json',
            xhrFields: {
                withCredentials: true
            },
            contentType: 'application/json; charset=utf-8',
            url: options.serviceUrl + '/DoStuff'
        });


Unfortunately, the functionality related to sending authorization data has become available for IE only since version 10, IE8 / 9 browsers do not support sending this information, and can only work with GET and POST.

Login

In all approaches above, authentication data from the main site is implicitly used. Having authorization on the main site bar.other, we are able to return user data by Ajax request from the site foo.example. In my case, this was used to enable the user to receive notifications and respond to events while being on one of the sites living as part of the same business project, but located on different domains and platforms. As already mentioned above, the key points here are the heading Access-Control-Allow-Credentialsand setting for the XmlHttpRequestflag withCredentials=true.

List of sources


Also popular now: