Welcome to my blog, stay tunned :
Home | Blogs | Stephane Eyskens's blog

SharePoint 2013 - Integration Challenges - #4 Same Origin Policy & Authentication - CORS

This blog post was moved to https://stephaneeyskens.wordpress.com/2014/01/29/sharepoint-2013-integra...


It turns out that at the time of writing, all the browsers are not yet compatible with CORS nor all the servers such as IIS if the targeted domain requires to be authenticated which is usually the case in SharePoint.
Because of that, preflight requests get a 401 (Unauthorized) answer from the server while it expects a 200. Therefore, for browsers that respect the CORS specification, if they receive anything else but 200, they should not perform the actual cross-domain request.
Moreover, the HTTP request headers that should be part of a server response will not appear by magic, so it requires some extra configuration at the web server level.


Which approach to choose?


There are several approaches you can use in order to get things working with IIS and therefore, with SharePoint. A good blog post on that topic can be found at http://evolpin.wordpress.com/2012/10/12/the-cors/. I have tested his approach and it's indeed working fine. In two words, in case you don't want to read his whole post, the approach consists in :

  • Adding the necessary HTTP response headers Access-Control-Allow-Headers, Access-Control-Allow-Methods and Access-Control-Allow-Origin at IIS level. In a SharePoint context, you can add those headers for a given web app using the IIS console
  • Develop a HTTP module to work around the authentication problem regarding the preflight requests
  • public class CORSPreflightModule : IHttpModule
    {
        private const string OPTIONSHEADER = "OPTIONS";
        private const string ORIGINHEADER = "ORIGIN";
        private const string ALLOWEDORIGIN = "http://....";
        void IHttpModule.Dispose()
        {
    
        }
    
        void IHttpModule.Init(HttpApplication context)
        {
            context.PreSendRequestHeaders += (sender, e) =>
            {
                var response = context.Response;
    
                if (context.Request.HttpMethod.ToUpperInvariant() == OPTIONSHEADER.ToUpperInvariant() &&
                    context.Request.Headers[ORIGINHEADER] == ALLOWEDORIGIN)
                {
                    response.StatusCode = (int)HttpStatusCode.OK;
                }               
            };
    
        }
    }
    



An alternative approach using a Reverse Proxy

So, instead of repeating what the other blogger said, let me propose an alternative approach. The HTTP module one is not my prefered option since it creates an unnecessary overhead at SharePoint level for every single request even those that are not concerned by CORS. As an alternative, if you have a Reverse Proxy at your disposal, you can achieve the same goal by placing the CORS logic at another level.

In development environments, you usually don't benefit from Enterprise reverse proxies. Therefore, let's see with our best friend Fiddler how to get the same results and get CORS up & running with SharePoint on your dev box. With this method, IIS is left untouched and no custom HTTP Module is required and if the final solution in the other environments use reverse-proxies, you'll be closer to the reality.

  • Open Fiddler, Rules, Customize Rules
  • Once in the file, find the OnBeforeResponse function and add the following
    static function SetCORSResponseHeaders(oSession: Session,origin: String)
    {
     oSession.oResponse.headers.Add("Access-Control-Allow-Headers","Content-Type,Authorization");
     oSession.oResponse.headers.Add("Access-Control-Allow-Methods","GET,POST,PUT,DELETE,MERGE");
     oSession.oResponse.headers.Add("Access-Control-Allow-Origin",origin);
     oSession.oResponse.headers.Add("Access-Control-Allow-Credentials",true);   
    }
    
    static function OnBeforeResponse(oSession: Session) {
    var origin:String="http://urlofallowedorigin"; //do not include a ending / character
    //in this case, we know we're in a CORS context since the origin header was sent
    if(oSession.oRequest.headers["Origin"] != '')
    {
      SetCORSResponseHeaders(oSession,origin);
     //In case we receive a preflight request
     if(oSession.oRequest.headers.HTTPMethod=='OPTIONS'    
      && oSession.oRequest.headers["Origin"]==origin)
     {    
      oSession.oResponse.headers.HTTPResponseCode  = 200;
      oSession.oResponse.headers.HTTPResponseStatus = "200 OK"; 
     }
    }
    


    In this case, I'm checking whether the ORIGIN request header is present or not which indicates if a CORS operation is ongoing. If it's indeed a CORS related request, I inject the HTTP headers required by CORS (+credentials which I'll detail later). Then, I check whether the method is OPTIONS which identifies a preflight request. If it's a prefight, I return the status code 200 (instead of default IIS 401). So, with that basic rule, I'm covering both preflight requests and actual requests targeting the remote domain. I could have also checked the current uri to make sure that I want to allow a given ORIGIN to contact a given target of course. Also, since the reverse proxy, in this case, Fiddler, sits between the browser and the server, you can easily configure rules the other way around by inverting the source & the target.



Client request and authentication



$.support.cors=true;
$.ajax({
    url:"remote domain/_api/web/lists/getbytitle('Documents')/itemCount",
    xhrFields: {withCredentials: true},
     headers:{ "Accept": "application/json; odata=verbose" },
     contentType: "application/json; odata=verbose"    
  }).done(function(data, textStatus){
     if (data.d) {
       $('#doccount').text("Number of documents in documents:"+data.d.ItemCount);
     }
  }).fail(function(){
      $('#doccount').text("failed");
  });

So, the query is very similar to a usual query. I just tell jQuery that I'm using CORS and I asked it to forward the credentials via the withCredentials attribute. For that to work fine, you need the server to send back the Access-Control-Allow-Credentials response header with the value true. If you have never authenticated to the remote domain at the time the AJAX query is sent, you're likely going to be prompted (with Chrome/Firefox). With IE, you won't be prompted if the target URL is in the Trusted/Intranet zone. This statement is valid for Claims - NTLM. If you use FBA, the browser will forward the Cookie if it already exists, meaning you have already authenticated before the AJAX request takes place otherwise, you'll get a 403 which you could handle in code and redirect to the login page...Of course, as I stated in another post, you can also use Fiddler (or your production proxy) to inject authentication information on the fly but I'm not going to repeat those steps in this post.



Environment I used for my tests

  • SharePoint 2013
  • IIS 7.5.7600.16385
  • Chrome 32.0.1700.102
  • Firefox 24.0
  • Internet Explorer 10
  • jquery1-9-1.min.js
  • Authentication : Claims - NTML & FBA



Pros & Cons of CORS in combination with SharePoint 2013

Let's start with the Cons:

  • At the time of writing, it's not supported by IE 8/9 (at least not conform to the CORS specification) which is already quite a strong limitation in many enterprises.
  • Even Firefox & Chrome have some different ways of handling the specification but they comply with most of it
  • As you could see, it requires some efforts server-side to get up & running

No, let's see the Pros:

  • When it will be fully supported and a common framework, it's a very good way to control who can talk to who and to have a granular control over what the caller can do (read? write?...)
  • With the web/mobile apps expanding a lot, CORS will be a common architecture very soon



But why using CORS via a reverse proxy?


If you read one of my posts about the same-origin policy http://www.silver-it.com/node/150 you might wonder why not just applying the technique it described via the reverse proxy instead of creating CORS specific rules. I think that the technique described in that post ends up in lying to the browser while if you place CORS specific rules at the reverse proxy level, you don't lie to the browser and the same-origin policy is still taking place. Morever, you realy make use of an emerging standard, the only difference is that you place the configuration one step above.
However, lying to the browser comes with one advantage : it's not browser/server dependent and will always work while CORS still has some limitations regarding portability. So, depending on your environment, you might chose what best suits you.

Happy Coding!









Comments

GET good, POST not working

I use this approach for GET requests fine, is POST also supposed to working similarly? Did you get POST to work with your approach - need to insert records?

In sharepoint online, how can i make it work ?

I need to authenticate user from the java script . I am using the normal approach provided on the internet to send the soap request with the user name and password. But i am getting the cross domain issue.

Browser is sending the options request instead of post. How can i used the above approach (reverse proxy) in sharepoint online ?

How to do that with SharePoint Online

Hi,

The reverse-proxy approach can be taken whatever is your target. However, with SharePoint Online, if you put a reverse proxy between your internal users and SPO, it will work only when people are inside the enterprise. If they connect from home, you won't have the possibility to put a reverse proxy.

SPO isn't CORS enabled and there is no way to enable CORS for SPO since you can't deploy any server-side component. In that case, you'd go for a SharePoint Add-In (previously known as Apps) and enable CORS in your Remote Web.

So, if your JavaScript code runs somewhere on-prem (or elsewhere), you'd make AJAX calls to your remote web and that guy would come back to SharePoint. You have one more hop so you might encounter a performance degradation but this should work. In a nutshell, your Remote Web would become a proxy for your other web site where your JavaScript code lives.

Best Regards