Introduction

Your application may require calling a Web API endpoint hosted on a remote Web Server from the js files residing in the SharePoint Online or any server. JS file can be in the SharePoint library, CDN or part of SPFX solution. This topic is gaining momentum as more assets move to hybrid architecture and mobile apps, the more is the need for a clean integration options. In coming years, this technique is expected to mature further and become less cumbersome.
In our discussion the imaginary remote Web API is running on a different domain. This makes the AJAX call cross-domain i.e., JS file loading to the browser from
https://mysite.sharepoint.com/ and makes API call to https://yoursite.com/. Cross domain calls are subject to additional scrutiny by the browser. More on CORS are available here. Http GET has some leniency, but API calls with http verbs like PUT/POST etc which intend to change data are subject to preflight process. Browsers are the gatekeeper here. Browser settings may allow to override CORS but for internet facing applications and mobile apps cannot afford to ask their users to lower their browser security to allow the cross domain scripts to run! CORS setup is more on the server side. Without sever configured, CORS calls will fail with HTTP 400/401.

Step 1

Here we are taking an example of CORS implementation using a JS file hosted in the SharePoint Online library and a Web API hosted on a remote IIS server (not Azure).

Ingredients:

On SharePoint

  • SharePoint Online Content Editor Web part / Any technology which allows to run JS code.
  • jQuery 3.*

On remote web server

  • .NET 4.6 on Windows Server 2012
On client device: 

Step 2

The remote server must be exposed for access as per the situation. For example, if you have a server, ensure it is accessible over the internet. It may require certification. In case your server is part of a Domain Controller and you intent to connect it from the SharePoint Online you may you use AD connect or ADFS configuration. If you are developing for an enterprise, perhaps these infrastructure is already existing. In this example, we will use windows credential based solution.

JS Code snippet running on the SharePoint

 

01.$.ajax({
02.            type: 'POST',
04.            contentType: 'application/json',
05.            async: true,          
06.            cache: false,
07.            xhrFields: {
08.                withCredentials: true
09.            },
10.        }).done(function (data) {
11.            //Success -- write code here.
12.            alert("Success");
13.        }).fail(function (error) {
14.            // Failure -- Get more from error.statusText           
15.             alert("Failure: " + error.statusText);
16.        });
If you omit xhrFields:{} attribute, it will not send windows credential to the server. If the server is configured for windows credential, the call will fail. If you notice, we did not put any CORS effort in this ajax call.


In the visual studio please create a Web API solution if not done already. You can find more on how to create Web API solution in the Microsoft documentation. Under the solution explorer please select the Web API project and expand the Reference section. Ensure you have System.Web.Cors and System.Web.Http.Cors. No worries if you don’t have those, usually they don’t come preloaded. Just go to Tools menu and get these through NuGet Package Manager. Once you are done with those references, please open the WebApiConfig.cs file under the App_Start folder of your Web API project. Add config.EnableCors(); in the Register(..) method of WebApiConfig class. If you are planning some advanced use of CORS like selective controllers or methods of the controller to be CORS enabled, you need to change the pattern. For starters, this will ensure all controllers are CORS exposed.

 

public static void Register(HttpConfiguration config)
{
    // Web API configuration and services          
    config.EnableCors();
    config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );            
}

Lets move to the web.config file.

 

 

Web.config file snippet of the Web API

<system.webServer>
  <httpProtocol>
      <customHeaders>
       <add name="Access-Control-Allow-Origin" value="https://mysite.sharepoint.com/" />      
        <add name="Access-Control-Allow-Headers" value="Content-Type, Authorization, Origin, Accept-Encoding, Accept-Language, Connection, Content-Length, Referer, User-Agent" />     
        <add name="Access-Control-Allow-Methods" value="GET, POST, PUT, HEAD, DELETE, OPTIONS, CONNECT, TRACE, PATCH" />
        <add name="Access-Control-Max-Age" value="3628800" />
        <add name="Access-Control-Allow-Credentials" value="true" />
      </customHeaders>
    </httpProtocol>
</system.webServer>

In the above snippet of web.config of the given Web API, Access-Control-Allow-Origin is binding to the trusted origin. In our example ajax call is originating from https://mysite.sharepoint.com/. If you want to add more trusted origins, you can comma separate the URLs. Theoretically you can put “*” to allow any origin. Until this CORS topic matures further with the advent of the hybrid and multi-cloud solutions; “*” is not honoured by many browsers. During processing of preflight response, some browsers snap conversation with the server if they find “*” in the response header.

Access-Control-Allow-Headers and Access-Control-Allow-Methods custom headers allow server admins to decide what all method verbs and Content-Type headers to allow. In this example, it is almost all, but it is open to the individual’s policy and practice in place in this regard.

The Access-Control-Max-Age header indicates how long the results of a preflight request can be cached in a preflight result cache. The Access-Control-Allow-Credentials header indicates whether the response to request can be exposed when the omit credentials flag is unset. When part of the response to a preflight request it indicates that the actual request can include user credentials. If you omit Access-Control-Allow-Credentials header, server will not expect any credential in the CORS call.

Visual studio usually takes care of the <handlers> section of the web.config. If not done automatically, ensure the following entries are there inside the <system.webServer> tag.
<system.webServer>
<handlers>
      <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
      <remove name="OPTIONSVerbHandler" />
      <remove name="TRACEVerbHandler" />
      <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
    </handlers>
</system.webServer>

On the IIS server, please enable windows authentication for this particular site as the only authentication method if you are keeping Access-Control-Allow-Credentials’s value as “true” in the config.web file.

This far, server side configuration appears complete and it is supposed to work. Then there are surprises!

Step 3

Preflight is bit tricky. As the term suggests – pre flight; browser first goes to server and submits the http verb “intent” without actual payload i.e., “pre” submission. Browser expects a certain http response from the server against this preflight request. If server meets the expectation, rest of the conversation follows. If server or IIS swings slightly different than the exact expectation, browser cancels the rest of the conversation. If you want to control that conversation between the browser and server, you need to use http listener facility to chip into the conversation and express your voice. Key expectations from the server; are

Access-Control-Allow-Origin header containing the domain from which browser is loading and the http response status is either 200/204. We already discussed about the Access-Control-Allow-Origin header setup in the web.config. For http response status, I personally prefer 204 as it is more truthful in the context of preflight response. Http response status 204 says “The server successfully processed the request and is not returning any content”.

Steps to add a http listener to the Visual Studio project  

  1. Add a class to the project.
  2. Copy paste the following code snippet into the class and update the namespace as per your solution.
01.using System;
02.using System.Collections.Generic;
03.using System.Linq;
04.using System.Web;
05.using System.Web.Http;
06.using System.Net;
07.
08.namespace mywebapp
09.{  
10.    public class HttpListener : IHttpModule
11.    {       
12.        public void Dispose() { }      
13.        public void Init(HttpApplication context)
14.        {
15.            context.PreSendRequestHeaders += delegate
16.            {
17.                if (context.Request.HttpMethod.ToUpper() == "OPTIONS" &&
18.                context.Request.Headers.AllKeys.Contains("Origin",StringComparer.InvariantCultureIgnoreCase))
19.                {
20.                    var response = context.Response;
21.                    response.StatusCode = (int)HttpStatusCode.NoContent;                   
22.                }
23.            };
24.        }
25.    }   
26.}

3. Open web.config and add the following <modules> binding to this HttpListener class.

<system.webServer>
<modules>
    <add name="HttpListener" type="mywebapp.HttpListener"/>
  </modules>
</system.webServer>

Init() of the HttpListener class will ensure http response returned to the browser contains the requisite response status. This HttpListener class will be part of the application DLL that will be deployed to IIS. No IIS configuration needs to be touched. This listener mechanism will not cause any interference to any other website running on the same IIS as this will be invoked within this application’s context only. Furthermore, it is wired through this given application’s web.config file. Hence no global effect to the IIS behaviour.

References