📜 ⬆️ ⬇️

WCF + Cross Domain Ajax Calls (CORS) + Authorization

Good day!
I would like to demonstrate one of the possible approaches to solving the problem of working with WCF services from different 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 to interact WCF and AJAX POST requests, including information about cookies and authorization.

As you know, just because AJAX call to another domain does not work, due to security reasons. To solve this problem, the CORS ( wiki , mozilla ) standard was invented and released. 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 the HTTP Origin header, the server must respond using the Access-Control-Allow-Origin header. Example of request / response pair from address foo.example foo.example on service bar.other/resources/public-data bar.other/resources/public-data :
Request:
GET / resources / public-data / HTTP / 1.1
Host: bar.other
Origin: foo.example
[Other headers]

Answer:
HTTP / 1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Access-Control-Allow-Origin: *
Content-Type: application / xml
')
[XML Data]


Headlines


In general, restrictions are imposed by the browser. If he doesn’t like something in the headers, he will not give this data to the user (unless the required Access-Control-Allow-Headers returns, or to the server, unless Access-Control-Allow-Credentials and the correct Access-Control-Allow-Origin . Before a POST request to another domain, the browser will first make an OPTIONS request ( preflight request ) for information on the allowed methods of working with the service.

WCF

On this topic there is a certain amount of information of various quality. Unfortunately, WCF does not allow standard headers to use these headers, however there are several options for solving this problem. I bring to your attention some of them.

Solution using web.config.

This solution involves adding the necessary headers directly to the web.config.
 <system.webServer> <httpProtocol> <customHeaders> <add name="Access-Control-Allow-Origin" value="http://foo.example" /> <add name="Access-Control-Allow-Headers" value="Content-Type" /> <add name="Access-Control-Allow-Methods" value="POST, GET, OPTIONS" /> <add name="Access-Control-Allow-Credentials" value="true" /> </customHeaders> </httpProtocol> </system.webServer> 

Differs in its simplicity and inflexibility. In particular, this particular example cannot be used if there are more than one possible domain, besides 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 extends to the entire site. Of course, all the conditions for specific services can be prescribed immediately, but in my opinion this is associated with the inconvenience of supporting the list of allowed services.

Solution with adding headers in WCF service code

This solution differs from the previous one only in that headers are added for a specific service or method. In general, the solution looks like this:
 [ServiceContract] public class MyService { [OperationContract] [WebInvoke(Method = "POST", ...)] public string DoStuff() { AddCorsHeaders(); return "<Data>"; } 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 the AddCorsHeaders call AddCorsHeaders required in each service method. Plus - ease of use.

Solution using native EndPointBehavior and DispatchMessageInspector

This approach takes advantage of WCF functionality.
2 classes of EnableCorsBehavior are created:
 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.config created by EnableCorsBehavior :
 <system.serviceModel> ... <extensions> <behaviorExtensions> <add name="crossOriginResourceSharingBehavior" type="My.Web.Cors.EnableCorsBehavior, My.Web, Version=1.0.0.0, Culture=neutral" /> </behaviorExtensions> </extensions> ... </system.serviceModel> 

We find and add the extension created for EnableCorsBehavior to the Behavior configuration of our Endpoint 'a
 <system.serviceModel> <services> <service name="My.Web.Services.MyService"> <endpoint address="" behaviorConfiguration="My.Web.Services.MyService" binding="webHttpBinding" contract="My.Web.Services.MyService" /> </service> </services> ... <behaviors> ... <endpointBehaviors> ... <behavior name="My.Web.Services.MyService"> <webHttp/> <crossOriginResourceSharingBehavior /> <!--     --> </behavior> ... </endpointBehaviors> ... </behaviors> ... </system.serviceModel> 

It remains for us to only process the preliminary request with the OPTIONS method. In my case, I used the simplest option: the OPTIONS request handler method is added to the service body.
 [OperationContract] [WebInvoke(Method = "OPTIONS", UriTemplate = "*")] public void GetOptions() { //    EnableCorsMessageInspector } 

Of course, there is a similar WCF extension for working with preflight requests, one of which can be read from the reference list at the end of the article. The main disadvantage is the need to add the GetOptions method to the service body and a considerable amount of additional code. On the other hand, this approach allows almost completely separating the logic of the 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 withCredentials flag is set to true withCredentials flag. I think that many people use jQuery to work with AJAX, so I’ll 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 associated with sending authorization data has become available for IE only from version 10, IE8 / 9 browsers do not support sending this information, and are able to work only with GET and POST.

Authorization

All approaches above implicitly use authentication data from the main site. Having authorization on the main site bar.other bar.other , we have the ability to return user data for an Ajax request from the site foo.example foo.example . In my case, this was used to enable the user to receive notifications and respond to events, being on one of the sites living within the same business project, but located on different domains and platforms. As mentioned above, the key points here are the header Access-Control-Allow-Credentials and setting the flag for XmlHttpRequest withCredentials=true .

List of sources

Source: https://habr.com/ru/post/219895/


All Articles