Inconvenient ADAL JS Angular with simultaneous CORS requests

dev.office.com banner
The ADAL JS Angular library offers great capabilities for building rich web applications unless you're trying to issue multiple CORS calls at the same time.

Angular - it's for SPAs

Angular is a popular framework for building Single Page Applications. It offers many different capabilities such as data binding or routing management making it useful for both small and large applications.

Angular is in fact so popular, that when Microsoft released support for CORS on Azure and Office 365, shortly thereafter they released the ADAL JS Angular library to help Angular developers build Single Page Applications that communicate with Office 365 and Azure using nothing else than JavaScript.

ADAL JS Angular

The ADAL JS Angular library makes it easy to instrument your application for communicating with Azure AD. The library provides a request interceptor which checks whether the request that you are issuing is a CORS request to Azure or Office 365 and if it is, it automatically takes care of getting the proper access token for you.

The ADAL JS Angular library simplifies building Single Page Applications that communicate with Azure and Office 365 a lot. That is as long as you're issuing CORS requests one at a time.

Inconvenient ADAL JS Angular with multiple simultaneous CORS requests

Imagine that you are building a Single Page Application that consists of a view with a few partial views on it. These partials views have each their own controller which issue CORS requests to Office 365 to get some data. In such scenario, on the initial page load what you would see is that only one of the requests completed.

It turns out that this issue is caused by how the ADAL JS Angular library obtains access tokens for resources. When you issue a request, the ADAL JS Angular library uses the underlying ADAL JS library to check if the access token for the particular resource has already been retrieved. If it has, it augments your request with it and your request is executed just as expected.

If the access token is not available however, the ADAL JS library will add an iframe to your page and use it to retrieve the specific access token and proceed with your request. This hidden iframe uses an ID based on the ID of the resource that you are trying to access. Unfortunately, if you issued multiple requests short one after another, when no access token is available yet, these calls will overwrite the previously created iframe aborting previous requests. Given that different controllers issue these requests, there is little that you can do to control how they are executed, but there seems to be a workaround that just seems to work.

Issuing multiple CORS requests simultaneously with the ADAL JS Angular library

ADAL JS keeps track of requested and retrieved access tokens for the specific resources. Before issuing your CORS request you could check whether the access token for the related resource is already available and if not, if it's already been requested. If it has, you would wait with executing your request until the access token for your resource becomes available.

Consider you had an Angular service in your Single Page Application with two functions to retrieve data from Office 365:

define([], function() {  
    app.factory('o365', o365);

    o365.$inject = ['$http', '$q'];

    function o365($http, $q) {        
        return {
            getListItems: getListItems,
            findDocument: findDocuments
        };
    }

    function getListItems() {
        var deferred = $q.defer();

        $http({
            // omitted for brevity
        }).success(function(data) {
            // omitted for brevity
        });

        return deferred.promise;
    }

    function findDocuments(searchQuery) {
        var deferred = $q.defer();

        $http({
            // omitted for brevity
        }).success(function(data) {
            // omitted for brevity
        });

        return deferred.promise;
    }
}

The first thing that you need to do, is to give your service access to adalProvider registered with your Single Page Application. adalProvider contains information about your SPA and its configuration. Your service also needs access to an instance of AuthenticationContext which allows you to get the information about access tokens.

define(['adal'], function(AuthenticationContext) {  
    app.factory('o365', o365);

    o365.$inject = ['$http', '$q', '$timeout', 'adalAuthenticationService'];

    function o365($http, $q, $timeout, adalProvider) {
        var self = this;

        self.adal = new AuthenticationContext(adalProvider.config);
        self.sleepInterval = 100;

        return {
            // omitted for brevity
        };
    }
}

In the sample above adal in the define clause refers to the ADAL JS module. Information about access tokens isn't stored in an instance of AuthenticationContext but in the local or session storage depending on the configuration of your SPA. So to access it, all we need to do, is to create a new instance of AuthenticationContext passing to it our SPA's configuration.

The service also gets injected the $timeout service to be able to delay requests if necessary to wait for the access token to become available.

To check whether an access token for the particular resource is available you can use the following function:

function accessTokenRequestInProgress(endpoint, adal) {  
    var requestInProgress = false;

    var resource = adal.getResourceForEndpoint(endpoint);

    var keysString = adal._getItem(adal.CONSTANTS.STORAGE.TOKEN_KEYS) || '';
    if (keysString.indexOf(resource + adal.CONSTANTS.RESOURCE_DELIMETER) > -1) {
        var tokenStored = adal.getCachedToken(resource);

        if (tokenStored === null) {
            requestInProgress = true;
        }
    }

    return requestInProgress;
}

This function requires two parameters: the URL that you want to call to retrieve the data from and an instance of AuthenticationContext we created earlier. Using the API of AuthenticationContext the function checks whether an access token for the particular resource has already been requested and if so, if it's available or not. Whenever an access token is requested, the corresponding resource ID is stored in the CONSTANTS.STORAGE.TOKEN_KEYS property. Once the access token is retrieved, it can be obtained using the getCachedToken function. So if the resource ID is already present in the CONSTANTS.STORAGE.TOKEN_KEYS property but the access token is not yet available it means that the access token has already been requested.

The final step is to wrap $http requests in your service so that you can delay them if necessary to wait for the access token to become available:

function getListItems(parentDeferred) {  
    var deferred = parentDeferred || $q.defer();

    var url = 'https://mytenant.sharepoint.com/_api/...';

    if (accessTokenRequestInProgress(url, self.adal)) {
        $timeout(function() {
            getListItems(deferred);
        }, self.sleepInterval);
    }
    else {
        $http({
            url: url,
            // omitted for brevity
        }).success(function(data) {
            // omitted for brevity
        });
    }

    return deferred.promise;
}

Using the URL of the $http request we first check whether the request for the particular access token is in progress. If it is, we delay the request passing the current deferred object as a parameter. If it's not, we execute the request and resolve or reject the promise. Either way we return the promise so that we can resolve it in the caller once the request is executed.

Following is the complete sample:

define(['adal'], function(AuthenticationContext) {  
    app.factory('o365', o365);

    o365.$inject = ['$http', '$q', '$timeout', 'adalAuthenticationService'];

    function o365($http, $q, $timeout, adalProvider) {
        var self = this;

        self.adal = new AuthenticationContext(adalProvider.config);
        self.sleepInterval = 100;

        return {
            getListItems: getListItems,
            findDocument: findDocuments
        };
    }

    function getListItems(parentDeferred) {
        var deferred = parentDeferred || $q.defer();

        var url = 'https://mytenant.sharepoint.com/_api/...';

        if (accessTokenRequestInProgress(url, self.adal)) {
            $timeout(function() {
                getListItems(deferred);
            }, self.sleepInterval);
        }
        else {
            $http({
                url: url,
                // omitted for brevity
            }).success(function(data) {
                // omitted for brevity
            });
        }

        return deferred.promise;
    }

    function findDocuments(searchQuery, parentDeferred) {
        var deferred = parentDeferred || $q.defer();

        var url = 'https://mytenant.sharepoint.com/_api/...';

        if (accessTokenRequestInProgress(url, self.adal)) {
            $timeout(function() {
                findDocuments(searchQuery, deferred);
            }, self.sleepInterval);
        }
        else {
            $http({
                url: url
                // omitted for brevity
            }).success(function(data) {
                // omitted for brevity
            });
        }

        return deferred.promise;
    }

    function accessTokenRequestInProgress(endpoint, adal) {
        var requestInProgress = false;

        var resource = adal.getResourceForEndpoint(endpoint);

        var keysString = adal._getItem(adal.CONSTANTS.STORAGE.TOKEN_KEYS) || '';
        if (keysString.indexOf(resource + adal.CONSTANTS.RESOURCE_DELIMETER) > -1) {
            var tokenStored = adal.getCachedToken(resource);

            if (tokenStored === null) {
                requestInProgress = true;
            }
        }

        return requestInProgress;
    }
}

Although this approach adds complexity to CORS requests, it is still relatively easy to understand and alter if necessary. This approach isn't by any means a best practice. It just works and I'm interested to hear what you think about it. Ideally the ADAL JS Angular library would take care of it all, but for the time being this seems like an acceptable workaround.

Summary

The ADAL JS Angular library simplifies building Single Page Applications that communicate with Azure and Office 365. Unfortunately, if you're issuing multiple CORS request to the same resources simultaneously only one of them will get resolved. By checking if a particular access token has already been requested and retrieved yourself, you will be able to issue multiple CORS requests to the same resources in parallel.

Comments

comments powered by Disqus