Consuming Azure Hosted Web API from SharePoint Online using JavaScript and Office 365 identities

Hi,

Consuming a web service from JavaScript has really become a commodity nowadays with the rise of SPA and other JavaScript based UIs. While this is quite easy to achieve in SharePoint On-Premises, it can be more challenging in SharePoint Online.

In SharePoint On-Premises, you can simply deploy your web services within SharePoint and target them via _vti_bin or you can even create your own REST endpoint and target them via _api/myendpoint…

In SharePoint Online, it isn’t possible to deploy custom web services nor custom REST endpoints since both require a FARM solution to be deployed. So, if you want to build a fancy UI and you need to get/update data that lives outside of SharePoint Online, it becomes tricky since the out of the box SharePoint web services/endpoints won’t help. Of course, if you develop a Provider-Hosted App, you could just handle your fancy UI in your remote web but sometimes, you just need to change out of the box pages and/or add some small controls here and there that cannot really be included within an App. For instance, if you want to check the confidentiality of a Team Site at page load and take appropriate action, you’d typically need to add a piece of script in a masterpage or as a User Custom Action and if, from such a script, you need to consume a remote service, things can become tricky. This is that kind of scenario that I’m tackling here.

Before jumping into solution mode, let’s see what challenges we need to tackle : 1) JavaScript in SPO and a service hosted elsewhere means cross-domain issue which is the reason why we’ll use CORS. 2) If you want to secure your web service so that it can only be consumed by authenticated users, you need to forward the user identity.

Therefore, here is an alternative approach that proves working quite fine. In a nutshell, here are some steps that help you achieving this goal:

  • Create an Azure Web Site that is secured with the Office 365 AD (configuration of the site)
  • Develop a web api service that is CORS compliant (use ASP.NET CORS package) and deploy it to the Azure Web Site
  • From SharePoint Online, build client-side components that use CORS to call the service….et voilà!

We’ll now see step by step how to do that.

Creating the Azure web site and securing the Azure Web Site

Here, you just need to go to manage.windowsazure.com and create a new web site. Open the Configure tab and add an authentication restriction by associating the Office 365 AD to your web app as shown below:

spows1

If it’s the first time you perform such a task, you can leave the default option that will create and associate a new App to your AD. It will use the name of your web site as the name of the AD app. If you want to reuse that AD app with other sites later, you can do it providing you’re keen on sharing the same configuration.
From there on, your web site should require an authenticate access.

Developing a webapi that is CORS enabled

ASP.NET Cross-Origin Resource Sharing nugget package can be added to your webapi project. However, at the time of writing, it’s not working very well on Azure. Additional steps must be undertaken to have a working CORS enabled service that can be consumed from 365.
Once the CORS package is added to your project, you can decorate your controller with some attributes as shown below:

[EnableCors(origins: "https://yourspodomain.sharepoint.com", headers: "Content-Type", methods: "get,post,put,delete,options",
   SupportsCredentials = true)]

Here is an example of a controller that supports CORS:
public class InputObject{
public string DummyData{
get;
set;
}
}

[EnableCors(origins: "https://yourspodomain.sharepoint.com", headers: "Content-Type", methods: "get,post,put,delete,options",
    SupportsCredentials = true)]
public class TestController : ApiController{
 public HttpResponseMessage Get(){
   return new HttpResponseMessage(){
     Content = (HttpContext.Current.User.Identity.IsAuthenticated)?
       new StringContent(HttpContext.Current.User.Identity.Name) : new StringContent("anonymous")
   };
 }
 public HttpResponseMessage Post(){
   return new HttpResponseMessage(){
     Content = (HttpContext.Current.User.Identity.IsAuthenticated)?
       new StringContent(HttpContext.Current.User.Identity.Name) : new StringContent("anonymous")
   };
 }
 public HttpResponseMessage Put(){
   return new HttpResponseMessage(){
     Content = (HttpContext.Current.User.Identity.IsAuthenticated)?
       new StringContent(HttpContext.Current.User.Identity.Name) : new StringContent("anonymous")
   };
 }
}

In the configuration class of the webapi, you must also enable CORS using this:

config.EnableCors();

Note that despites of the fact that allowed methods are specified using the EnableCors attribute, these headers are not returned as it should to the caller. Therefore, the HTTP module will not only handle the preflight request but will also add the necessary response headers to respect the CORS specification. Here is a code sample that illustrates how to handle this:

public class CORSPreflightModule : IHttpModule
    {
        private const string OPTIONSMETHOD = "OPTIONS";
        private const string ORIGINHEADER = "ORIGIN";
        private const string ALLOWEDORIGIN = "https://yourspodomain.sharepoint.com";
        void IHttpModule.Dispose()
        {

        }
        void IHttpModule.Init(HttpApplication context)
        {
            context.PreSendRequestHeaders += (sender, e) =>
            {
                var response = context.Response;

                if (context.Request.Headers[ORIGINHEADER] == ALLOWEDORIGIN)
                {
                    response.Headers.Add("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
                    response.Headers.Add("Access-Control-Allow-Headers", "Content-Type");
                }
                if (context.Request.HttpMethod.ToUpperInvariant() == OPTIONSMETHOD && context.Request.Headers[ORIGINHEADER] == ALLOWEDORIGIN)
                {
                    response.Headers.Clear();
                    response.Headers.Add("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
                    response.Headers.Add("Access-Control-Allow-Origin", "https://yourspodomain.sharepoint.com");
                    response.Headers.Add("Access-Control-Allow-Credentials", "true");
                    response.Headers.Add("Access-Control-Allow-Headers", "Content-Type");
                    response.Clear();
                    response.StatusCode = (int)HttpStatusCode.OK;
                }
            };

        }        

    }

For sake of simplicity, I didn’t put that in a web.config but information such as allowed methods, origin etc…should be stored in the web.config file. You should of course deploy that webapi to the Azure web site you secured with the 365 AAD.

Consuming the webapi from SharePoint Online and forwarding the 365 fedauth cookie

Browser Trick
In theory, you should be able to consume the web service already but it appears that the browser doesn’t handle it that way. If you start performing AJAX requests against the remote Azure web service, the browser will complain about missing CORS response headers. The reason why it does that is because your web site redirect it the 365 login page which isn’t CORS aware. The redirection is weird since the AADFederationCookie is correctly sent to the remote party so it should have been enough.
Unfortunately, until you open another tab against the remote web, AJAX queries will fail. Therefore, as a workaround, it is possible to inject a hidden iframe that points to the remote Azure web site in order to force the browser to authenticate against it. Since it already holds the authentication cookie from the 365 session, it will authenticate the iframe transparently. Here is a short script that performs the job. The idea is to inject that script into every page where this remote connection is required:

<script src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
<script type="text/javascript">
RemotePartLoaded=0;
$('<iframe src="https://yourazureservice.azurewebsites.net" style="display:none" onload="javascript:RemotePartLoaded=1;"></iframe>').appendTo('body');
function ExecuteOrDelayUntilRemotePartyLoaded(func) {
    if(RemotePartLoaded===1) {
	func();
    }
    else {
	console.log("not loaded");
	setTimeout(function(){ExecuteOrDelayUntilRemotePartyLoaded(func);},1000);
    }
}
</script>

In this example, the remote url is hard-coded but the idea is to get it loaded in a dynamic way.
You can consume the ExecuteOrDelayUntilRemotePartyLoaded function easily from your components afterwards:

ExecuteOrDelayUntilRemotePartyLoaded(function(){//do some AJAX against the Azure site here});

From there on, AJAX queries work fine.
Now that the Azure web site was secured with AAD and that a web service under that site responds with the appropriate response headers and that a hidden iframe pointing to the Azure web site is placed in the calling page, it can be consumed from SharePoint Online using JavaScript:

$.ajax({
 crossDomain: true,
 xhrFields: {
 'withCredentials': true
 },
 type: 'GET',
   url: 'https://yourazureservice.azurewebsites.net/api/test'
 }).done(function (data) {
   console.log(data);
 }).error(function (jqXHR, textStatus, errorThrown) {
   console.log('oops');
});

Note the presence of the withCredentials field which instructs the browser to forward the Office 365 credentials. You can consume your remote webapi service with GET/PUT/DELETE. For some reasons, the POST verb throws an Access Denied issue although authentication date is sent correctly. This will of course work with federated & non-federated domains.

Happy Coding!

Advertisements

About Stephane Eyskens

Office 365, Azure PaaS and SharePoint platform expert
This entry was posted in Office 365, SharePoint Online and tagged , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s