Building Office 365 Single Page Applications on any platform

dev.office.com banner
CORS simplifies building Office 365 SPAs. If you're however building multi-tenant applications on a stack without an SDK, there are a few things that you have to take care of.

Building Single Page Applications for a better user experience

Single Page Applications offer great performance and user experience benefits when building web applications. By leveraging capabilities of modern browsers you can dynamically load the resources necessary for the particular part of the application with which the user is interacting at the given moment. This allows your application to load faster offering a better user experience as a result.

100% JavaScript

With the recent release of support for CORS, the Office 365 team makes it easier for us to build Single Page Applications that leverage Office 365. CORS almost entirely eliminates the need for server-side code and allows your application to authenticate and interact with Office 365 APIs directly from JavaScript.

Many faces of CORS

As long as your Single Page Application is tied to a specific Office 365 tenant, you can fully benefit of CORS and have your application work completely using client-side code. For multi-tenant applications things are a bit more complicated.

One of the first things that a multi-tenant application interacting with Office 365 has to do, is to find the name of the Office 365 tenant that it should connect to. This will change in the future when the Office 365 Unified APIs will be released, but until then your application should use the Discovery Service. Unfortunately currently the Discovery Service doesn't support CORS which makes building multi-tenant Single Page Applications a bit more complex.

Workaround, but only with the SDK

If you're building Single Page Applications with the Office 365 SDK, there are a few resources that show you how to work around the current limitation of the Discovery Service, such as the sample built by Chaks. By building a server-side API to call the Discovery Service you can work around the lack of support for CORS and the Office 365 SDK will help you get things done. If you're not using the SDK however things are a bit more complicated.

In a regular server-side scenario, in order to call the Discovery Service, you would start the OAuth flow, get an access code which then you would exchange for an access token for the Discovery Service. This process would be orchestrated by redirects to Azure AD which, as you can imagine, have no place in a Single Page Application, unless if you don't want to introduce any inconveniences in the user experience. So how come that things just work when using the SDK?

CORS and Discovery Service for everyone

If you take a closer look at how things are done in the Office 365 SDK, you will find out that it uses the on-behalf-of OAuth call to exchange the access token retrieved as a part of the authentication flow for an access token for the Discovery Service. Currently it seems like there is no documentation describing this flow in more detail. Still it is possible to replicate what the Office 365 SDK is doing on the platform of your choice.

Exchanging the login access token for the Discovery Service access token

When using CORS you usually start by logging in to Azure AD:

var config = {  
    // your application configuration information omitted for brevity
};
var authContext = new AuthenticationContext(config);  
authContext.login();  

After the authentication completes, you will have access to the information about the authenticated user as well as the access token. The next part is to combine those two pieces of information and exchange the access token retrieved during the authentication for the access token for the Discovery Service. This can be done using the OAuth on-behalf-of flow.

The following code snippet illustrates how you would do this in Node.js but you can apply the same concept on any other platform capable of issuing a POST request.

var querystring = require('querystring'),  
    Q = require('q');

function getDiscoveryServiceAccessToken(loginAccessToken) {  
    var deferred = Q.defer();

    var payload = querystring.stringify({
        'resource': 'https://api.office.com/discovery/',
        'client_id': 'app-client-id',
        'client_secret': 'app-client-secret',
        'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
        'assertion': loginAccessToken,
        'requested_token_use': 'on_behalf_of',
        'scope': 'openid'
    });

    var postRequest = https.request({
        method: 'POST',
        hostname: 'login.microsoftonline.com',
        path: '/common/oauth2/token',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Content-Length': payload.length
        },
        secureOptions: require('constants').SSL_OP_NO_TLSv1_2,
        ciphers: 'ECDHE-RSA-AES256-SHA:AES256-SHA:RC4-SHA:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM',
        honorCipherOrder: true
    }, function(res) {
        var body = "";
        res.on("data", function (chunk) {
            body += chunk;
        });
        res.on("end", function () {
            try {
                var d = JSON.parse(body);

                if (d && d.access_token) {
                    deferred.resolve(d.access_token);
                }
                else {
                    deferred.reject('Unable to exchange login access token for Discovery Service access token');
                }
            }
            catch (ex) {
                deferred.reject(ex);
            }
        });
    }).on("error", function (err) {
        deferred.reject(err);
    });

    postRequest.write(payload);
    postRequest.end();

    return deferred.promise;
}

First, the POST request payload is constructed. It consists of the information about your application, as registered with Azure AD, (client_id and client_secret) and the name of the resource for which you want to request the access token (Discovery Service) - this is the same as in any other OAuth token flow. Then however, the payload specifies that this is an on-behalf-of request by setting the grant_type parameter to urn:ietf:params:oauth:grant-type:jwt-bearer, the requested_token_use parameter to on_behalf_of and the scope parameter to openid. The final part is to pass the access token obtained in the login step of your SPA through CORS in the assertion parameter.

If the request succeeds the response will contain an access token for the Discovery Service which is used to resolve the promise.

Calling the Discovery Service to get the URL of the Office 365 tenant

With the previously retrieved access token for the Discovery Service we can call the Discovery Service to get the URL of the Office 365 tenant of the currently logged user.

The following example shows how to get the URL of the root SharePoint Site but you could easily expand the sample including other services as necessary.

function getTenantUrl(accessToken) {  
    var deferred = Q.defer();

    https.get({
        hostname: 'api.office.com',
        path: "/discovery/v1.0/me/services([email protected]_SHAREPOINT')?$select=serviceResourceId",
        headers: {
            "Authorization": "Bearer " + accessToken,
            "Accept": "application/json"
        },
        secureOptions: require('constants').SSL_OP_NO_TLSv1_2,
        ciphers: 'ECDHE-RSA-AES256-SHA:AES256-SHA:RC4-SHA:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM',
        honorCipherOrder: true
    }, function (res) {
        var body = "";
        res.on("data", function (chunk) {
            body += chunk;
        });
        res.on("end", function () {
            try {
                var serviceInfo = JSON.parse(body);

                if (serviceInfo && serviceInfo.serviceResourceId) {
                    deferred.resolve(serviceInfo.serviceResourceId);
                }
                else {
                    deferred.reject('Invalid response from Discovery Service');
                }
            }
            catch (ex) {
                deferred.reject(ex);
            }
        });
    }).on("error", function (err) {
        deferred.reject(err);
    });

    return deferred.promise;
}

If you read my previous post about completing the OAuth flow using REST you won't see anything new in the code snippet above. It's a regular call to the Discovery Service optimized to only retrieve the URL of the root SharePoint Site which resolves the promise if the request succeeded.

From client to server: passing the login access token to the server-side Discovery Service wrapper

As we are building a SPA using CORS the access token that we get as the result of login is stored on the client. However, in order to call the Discovery Service using our server-side wrapper, we need to transfer it to the server. You could think of a number of ways to do that but if you look at the .NET sample I mentioned above you will see that the access token retrieved initially is passed in the Authorization request header.

Here is how it would look when calling the Discovery Service wrapper from your SPA:

function getTenantUrl(accessToken) {  
    var deferred = $.Deferred();

    $.ajax({
        url: '/api/discovery',
        method: 'GET',
        headers: {
            'Authorization': 'Bearer ' + accessToken
        },
        cache: true
    }).then(function(data) {
        if (data.tenantUrl) {
            deferred.resolve(data.tenantUrl);
        }
        else {
            deferred.reject('Could not get tenant URL');
        }
    }, function(err) {
       deferred.reject(err); 
    });

    return deferred;
}

function readDataFromSharePoint() {  
    getTenantUrl(authContext.getCachedToken(authContext.getCachedUser().profile.aud)).then(function(tenantUrl) {
        authContext.acquireToken(tenantUrl, function(error, token) {
            // call SharePoint here
        });
    }, function(err) {
        // handle error
    });
}

The authContext object is an instance of the AuthenticationContext class provided by the ADAL JS library. By calling its getCachedToken function you can retrieve an access token for the specified resource. For this however you need the resource ID, which in this case is the ID of the tenant to which the currently authenticated user belongs. This ID can be retrieved by getting the currently logged in user (authContext.getCachedUser()) and then retrieving the value of the profile.aud property. With the access token retrieved you can call our Discovery Service wrapper passing the access token in the Authorization header.

In the Discovery Service wrapper on the server you could build a route handler such as:

var express = require('express'),  
    router = express.Router();

router.get('/discovery',  
    function (req, res) {
        var tenantUrl = req.session['tenantUrl'];

        if (tenantUrl) {
            res.set('Content-Type', 'application/json');
            res.send({ tenantUrl: tenantUrl });
        }
        else {
            var accessToken = req.get('Authorization');
            if (accessToken.indexOf('Bearer ') === 0) {
                accessToken = accessToken.substr(7);

                getDiscoveryServiceAccessToken(accessToken).then(function(discoveryServiceAccessToken) {
                    getTenantUrl(discoveryServiceAccessToken).then(function(_tenantUrl) {
                        req.session['tenantUrl'] = _tenantUrl;
                        tenantUrl = _tenantUrl;

                        res.set('Content-Type', 'application/json');
                        res.send({ tenantUrl: tenantUrl });
                    }, function(err) {
                        res.status(400).send('Discovery failed');
                    }).done();
                }, function(err) {
                    res.status(400).send('Discovery failed');
                }).done();
            }
            else {
                res.status(400).send('Invalid token');
            }
        }
    });

When processing the request we first check if the request contains the access token in the Authorization header. If it does we remove the Bearer prefix. Optionally you could unpack the JWT token and perform some additional checks before passing it on to Azure AD. Then we call the helper functions we built previously to call the Discovery Service and get the URL of the SharePoint Site. Once completed we send the URL of the SharePoint Site as a JSON object.

To help you get the whole picture of how the different pieces are working together, I built a simple application using this approach and published it in a GitHub repo at https://github.com/waldekmastykarz/o365-cors-multitenantspa.

Summary

Support for CORS in Office 365 APIs simplifies building Single Page Applications. Thanks to CORS we can build SPAs almost entirely using client-side code which makes it easier to build and maintain those applications. If you're building multi-tenant SPAs some additional work is required as the Discovery Service currently doesn't support CORS. By building a simple server-side wrapper you will be able to provide your SPA with the necessary information about your Office 365 tenant and interact with it using CORS.

View the sample app on GitHub at https://github.com/waldekmastykarz/o365-cors-multitenantspa.

Comments

comments powered by Disqus