OAuth On-Behalf-Of Flow: Getting a user access token without user interaction. Includes ADAL sample.

OAuth 2.0 (and hence Azure Active Directory) provides the On-Behalf-Of flow to support obtaining a user access token for a resource with only a user access token for a different resource – and without user interaction.
This supports the scenario where a secured Web API acts as an interface to other resources (a.k.a endpoints) secured by the same identity provider and that require user context. As a practical example, a mobile client accesses some resources via a middle tier API which provides services such as data processing, caching, API simplification/optimisation, joining of datasets, etc.

The OAuth flow that achieves this is called the On-Behalf-Of flow; this makes sense as we’re facilitating the middle tier to act on behalf of the client when it accesses the resources farther down.

Using the on-behalf-of flow to access a resource via a middle tier API

Some background

Authentication with an OAuth 2.0 identity provider (such as AAD) produces JWT tokens. These tokens include information such as which claims (permissions) the user should be granted and the particular resource at which the token is valid (such as graph.microsoft.com). The OAuth 2.0 framework is specified such that a given token can only ever be valid for a single resource. This means that the token received by an endpoint (such as an Azure App Service Web API) cannot be used to directly authenticate to ‘another resource’. This is because the token’s resource will be that of the Web API and not the ‘other resource’. To see this I recommend checking out jwt.io and cracking open some tokens.

For completeness, the ‘other resource’ could be accessed using app-only authentication if it supports it, and if user context is not required (i.e. the return value will be the same regardless of the user) although this may greatly increase complexity in a multi-tenant scenario.

Configuring AAD for on-behalf-of

Before we get to the code the first hurdle is configuring AAD app registrations correctly. Initially it may be tempting to consider having both the Client and Web API layers utilise a single AAD app registration. After all, they are same holistic ‘app’ and how else can we get a user to consent to the permissions required by the Web API app when there is no interactive interface at that point? The latter point is resolved by explicitly binding the app registrations so that both are consented to as one. I mention how this is done below. By having two app registrations the flexibility of configuration is improved; we can have a Native app registration for the client and a Web API app registration for the Web API, we can have implicit flow configured for one app and not the other, and generally have granular control over configuration. Most vitally, an app registration can’t issue tokens valid for its own resource so two app registrations is a requirement.

I’ll avoid stepping though the configuration of the app registrations here as this is available elsewhere including this GitHub project. I will give a high level overview of what needs to happen.

    1. Create app registration for the Web API
      • Assign permissions to the downstream resources (e.g. Microsoft Graph, a custom Web API, etc)
      • If supporting multi-tenant authentication ensure availableToOtherTenants is set to true in the manifest
    2. Create app registration for the Client
      • Assign permissions to the app registration created above for the Web API
      • If supporting multi-tenant authentication ensure availableToOtherTenants is set to true in the manifest
    3. Associate app registrations
      • In the manifest for Web API app registration, configure knownClientApplications to reference the App ID for the app registration created for the Client. E.g. "knownClientApplications": ["9XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXc"]
        This binds the app registrations such that the Web API app registration is consented to as part of a single consent dialog displayed to a user when they authenticate to the Client app registration.
Before and after the app registrations are associated. Note how ‘Access Mobile App Backend’ is no longer present and instead is expanded to show the individual permissions required by that app.

Access Token Broker code

The following code is a .NET example of how to use the Active Directory Authentication Library (ADAL) to achieve the On-Behalf-Of flow.

Credits and further reading

This GitHub project was a very useful resource and a recommended starting point:
https://github.com/Azure-Samples/active-directory-dotnet-webapi-onbehalfof-ca

Microsoft docs OAuth 2.0 On-Behalf-Of flow
https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols-oauth-on-behalf-of

OAuth 2.0 specification
https://tools.ietf.org/html/rfc6749

Redis Token Cache example
https://blogs.msdn.microsoft.com/mrochon/2016/09/19/using-redis-as-adal-token-cache/

Vardhaman Deshpande – always helpful
http://www.vrdmn.com/

I was inspired to write this post not because this information isn’t available but because the information is hard to find if you aren’t familiar with the term “On-Behalf-Of”. Hopefully this post will be found by those of you searching for terms like “trade access token for new resource”, “change token resource”, “use access token with multiple resources/endpoints”, “access Microsoft Graph via Web API”, etc.

Paul.

OAuth Implicit Flow in JS without ADAL.js

This post provides a lightweight implementation of the OAuth implicit flow grant for obtaining an access token. Implicit flow is appropriate when the current user is authenticated to a common identity provider (e.g. Azure Active Directory a.k.a AAD) and the client (the environment requesting the token) is not secure. A great example of this is making a call to the Microsoft Graph from a page in SharePoint Online using only JavaScript.

The ADAL.js library exists as an authentication solution specifically for when working against AAD as the identity provider. Unfortunately, it is currently not well maintained and is over complicated. EDIT: ADAL.js has been updated multiple times since this post was first written and I would recommend using it. From a user experience perspective, the implementation discussed in this post avoids the need to redirect in order to authenticate. It happens seamlessly in the background via a hidden iframe.

Azure Active Directory
Azure Active Directory
  • A great article on the OAuth grants, agnostic of implementation, can be found here.
  • Thanks to my colleague Paul Lawrence for writing the first iteration of this code.
  • This code has a dependency on jQuery, mostly just for promises. I know, old school. I expect I’ll write an es6/2016 version of this soon enough but it shouldn’t be a challenge to convert this code yourself.
  • As I know I’ll get comments about it if I don’t mention it, this code doesn’t send and verify a state token as part of the grant flow. This is optional as far as the OAuth specification is concerned but it should be done as an additional security measure.
  • Although I’m Microsoft stack developer and have only tested this with AAD as the identity provider, I believe that it should work for any identify provider that adheres to the OAuth specification for authentication. You would need to play around with the authorisation server URL as login.microsoftonline.com is specifically for authenticating to AAD. I’d love feedback on this.
  • By definition, the OAuth implicit flow grant does not return a refresh token. Furthermore, the access token has a short lifetime, an hour I believe, and credentials must be re-entered before additional access tokens can be obtained via the implicit flow grant. The code provided in this post handles this by returning a URL which can be used to re-authenticate when a request fails. This URL can be used behind a link or redirection could be forced to occur automatically.

The following code snippet is an example of using this implicit flow library to call into the Microsoft Graph from within the context of a SharePoint Online page.
You will need to provide an appropriate AAD app ID for your AAD app. And don’t forget that you need to enable implicit flow via the app manifest and associate the correct delegate permissions.
This code should work not only with the Microsoft Graph but also to SharePoint Online endpoints, other AAD secured resources such as Azure services or your own AAD secured and CORS enabled web API.
[See note above about identity providers other than AAD]

var aadAppClientId = "8BE5AA0E-F900-4BDF-A7CF-71B3CC53B78E";
var resource = "https://graph.microsoft.com"
var query = "/v1.0/me/events";
var tokenFactory = new CC.CORE.Adal.AppTokenFactory(aadAppClientId, resource);
tokenFactory.ExecuteQuery(query)
.done(function (response) {
	// Success!
})
.fail(function (response) {
	// NOTE: Provide a link to renew an expired or yet to be approved session:
	// "Sorry, your session has expired or requires your approval. 
	// <div><a href='" + response.authorizeUrl + "'>Click here to sign in</a></div>";
});

Here is the implicit flow library code itself.

var CC = CC || {};
CC.CORE = CC.CORE || {};

CC.CORE.Log = function (errMsg) {
    // console.log is undefined in IE10 and earlier unless in debug mode, so must check for it
    if (typeof window.console === "object" && typeof console.log === "function") {
        console.log(errMsg);
    }
};

CC.CORE.Adal = (function () {
    "use strict";

    var appTokenFactory = function (aadAppClientId, resource) {
        // redirectUrl is the URL which the iframe will redirect to once auth occurs.
        // we use blank.gif as it is a very low payload
        var redirectUrl = _spPageContextInfo.webAbsoluteUrl + "/_layouts/images/blank.gif";

        // NOTE on security: include the userId in the cache key to prevent the case where a user logs out but
        // leaves the tab open and a new user logs in on the same tab. The first user's calender
        // would be returned if we didn't associate the cache key with the current user.
        var cacheKey = "candc_cache_adal_" + _spPageContextInfo.userId + "_" + aadAppClientId + "_" + resource;

        this.params = {
            clientId: aadAppClientId,
            redirectUrl: redirectUrl,
            resource: resource,
            cacheKey: cacheKey
        };

        var getAuthorizeUri = function (params, redirectUrl) {
            var authUri = "https://login.microsoftonline.com/common/oauth2/authorize" +
                            "?client_id=" + params.clientId +
                            "&response_type=token" +
                            "&redirect_uri=" + encodeURIComponent(redirectUrl) +
                            "&resource=" + encodeURIComponent(params.resource);
            return authUri;
        };

		var getQueryStringParameterByName = function (name, url) {
			name = name.replace(/[\[\]]/g, "\\$&");
			var regex = new RegExp("[?&#]" + name + "(=([^&#]*)|&|#|$)");
			var results = regex.exec(url);
			if (!results) return null;
			if (!results[2]) return '';
			return decodeURIComponent(results[2].replace(/\+/g, " "));
		};
		
        // create iframe, set its href, set listener for when loaded
        // to parse the query string. Deferred returns upon parse of query string in iframe.
        var acquirePassiveToken = function (params) {
            var deferred = jQuery.Deferred();

            // create iframe and inject into dom
            var iframe = jQuery("<iframe />").attr({
                width: 1,
                height: 1,
                src: getAuthorizeUri(params, params.redirectUrl)
            })
            jQuery(document.body).append(iframe);

            // bind event handler to iframe for parse query string on load
            iframe.on("load", function (iframeData) {
                parseAccessTokenFromIframe(iframeData, deferred);
            });

            return deferred.promise();
        };

        // handle iframe once it has loaded
        var parseAccessTokenFromIframe = function (iframeData, deferred) {
            // read the iframe href
            var frameHref = "";
            try {
                // this will throw a cross-domain error for any issue other than success
                // as the iframe will diplay the error on the login.microsoft domain
                frameHref = iframeData.currentTarget.contentWindow.location.href;
            }
            catch (error) {
                deferred.reject(error);
                return;
            }

            // parse iframe query string parameters
            var accessToken = getQueryStringParameterByName("access_token", frameHref);
            var expiresInSeconds = getQueryStringParameterByName("expires_in", frameHref);

            // delete the iframe, and event handler.
            var iframe = jQuery(iframeData.currentTarget);
            iframe.remove();

            // resolve promise
            deferred.resolve({
                accessToken: accessToken,
                expiresInSeconds: expiresInSeconds
            });
        };

        // get the most recent token from the cache, or if not available,
        // fetch a new token via iframe
        var getToken = function (params) {
            var deferred = jQuery.Deferred();
            
            // check for cached token
            var tokenFromCache = CC.CORE.Cache.Get(params.cacheKey);
            if (!tokenFromCache) {
                // fetch token via iframe
                acquirePassiveToken(params)
                .done(function (tokenFromIframe) {
                    CC.CORE.Log("ADAL: Fetched token from iframe.");
                    // expire cache a minute before token expires to be safe
                    var cacheTimeout = (tokenFromIframe.expiresInSeconds - 60) * 1000;
                    CC.CORE.Cache.Set(params.cacheKey, tokenFromIframe, cacheTimeout);
                    // resolve the promise
                    deferred.resolve(tokenFromIframe);
                })
                .fail(function (error) {
                    // Logs when rejection is caught
                    deferred.reject(error);
                });
            }
            else {
                CC.CORE.Log("ADAL: Fetched token from cache.");
                // resolve the promise
                deferred.resolve(tokenFromCache);
            }
            return deferred.promise();
        };

        this.ExecuteQuery = function (query, additionalHeaders) {
            var deferred = jQuery.Deferred();
            var params = this.params;
            // get token from cache or via iframe
            getToken(params)
            .done(function (token) {
                // submit request with token in header
                var ajaxHeaders = {
                    'Authorization': 'Bearer ' + token.accessToken
                };
                if (typeof additionalHeaders === "object") {
                    jQuery.extend(ajaxHeaders, additionalHeaders);
                }
                jQuery.ajax({
                    type: "GET",
                    url: params.resource + query,
                    headers: ajaxHeaders
                }).done(function (response) {
                    deferred.resolve(response);
                }).fail(function (error) {
                    deferred.reject({
                        error: error
                    });
                });
            })
            .fail(function (error) {
                CC.CORE.Log('ADAL error occurred: ' + error);
                deferred.reject({
                    error: error,
                    authorizeUrl: getAuthorizeUri(params, window.location.href)
                });
            });
            return deferred.promise();
        };
    };

    return {
        AppTokenFactory: appTokenFactory
    };

})(jQuery);

And here is the definition of the cache functions used above. Nothing special here, this could be swapped out with any cache implementation or removed altogether if caching is truly unnecessary or a security concern.

var CC = CC || {};
CC.CORE = CC.CORE || {};

CC.CORE.Cache = (function () {
    var defaultCacheExpiry = 15 * 60 * 1000; // default is 15 minutes
	var aMinuteInMs = (1000 * 60);
	var anHourInMs = aMinuteInMs * 60;
	
    var getCacheObject = function () {
        // Using session storage rather than local storage as caching benefit
        // is minimal so would rather have an easy way to reset it.
        return window.sessionStorage;
    };

    var isSupportStorage = function () {
        var cacheObj = getCacheObject();
        var supportsStorage = cacheObj && JSON && typeof JSON.parse === "function" && typeof JSON.stringify === "function";
        if (supportsStorage) {
            // Check for dodgy behaviour from iOS Safari in private browsing mode
            try {
                var testKey = "candc-cache-isSupportStorage-testKey";
                cacheObj[testKey] = "1";
                cacheObj.removeItem(testKey);
                return true;
            }
            catch (ex) {
                // Private browsing mode in iOS Safari, or possible full cache
            }
        }
        CC.CORE.Log("Browser does not support caching");
        return false;
    };

    var getExpiryKey = function (key) {
        return key + "_expiry";
    };

    var isCacheExpired = function (key) {
        var cacheExpiryString = getCacheObject()[getExpiryKey(key)];
        if (typeof cacheExpiryString === "string" && cacheExpiryString.length > 0) {
            var cacheExpiryInt = parseInt(cacheExpiryString);
            if (cacheExpiryInt > (new Date()).getTime()) {
                return false;
            }
        }
        return true;
    };

    var get = function (key) {
        if (isSupportStorage()) {
            if (!isCacheExpired(key)) {
                var valueString = getCacheObject()[key];
                if (typeof valueString === "string") {
                    CC.CORE.Log("Got from cache at key: " + key);
                    if (valueString.indexOf("{") === 0 || valueString.indexOf("[") === 0) {
                        var valueObj = JSON.parse(valueString);
                        return valueObj;
                    }
                    else {
                        return valueString;
                    }
                }
            }
            else {
                // remove expired entries?
                // not required as we will almost always be refreshing the cache
                // at this time
            }
        }
        return null;
    };

    var set = function (key, valueObj, validityPeriodMs) {
        var didSetInCache = false;
        if (isSupportStorage()) {
            // Get value as a string
            var cacheValue = undefined;
            if (valueObj === null || valueObj === undefined) {
                cacheValue = null;
            }
            else if (typeof valueObj === "object") {
                cacheValue = JSON.stringify(valueObj);
            }
            else if (typeof valueObj.toString === "function") {
                cacheValue = valueObj.toString();
            }
            else {
                alert("Cannot cache type: " + typeof valueObj);
            }

            // Cache value if it is valid
            if (cacheValue !== undefined) {
                // Cache value
                getCacheObject()[key] = cacheValue;
                // Ensure valid expiry period
                if (typeof validityPeriodMs !== "number" || validityPeriodMs < 1) {
                    validityPeriodMs = defaultCacheExpiry;
                }
                // Cache expiry
                getCacheObject()[getExpiryKey(key)] = ((new Date()).getTime() + validityPeriodMs).toString();
                CC.CORE.Log("Set in cache at key: " + key);
                didSetInCache = true;
            }
        }
        return didSetInCache;
    };

    var clear = function (key) {
        var cache = getCacheObject();
        cache.removeItem(key);
        cache.removeItem(getExpiryKey(key));
    };

    return {
        Get: get,
        Set: set,
        Clear: clear,
        IsSupportStorage: isSupportStorage,
        Timeout: {
            VeryShort: (aMinuteInMs * 1),
            Default: (anHourInMs * 2),
            VeryLong: (anHourInMs * 72),
        }
    };
})();

I welcome your comments, especially from anyone who gives this a go outside of Office 365 and the Microsoft stack.

Paul.

Azure AD app wildcard Reply URL

Azure AD apps (a.k.a Azure Active Directory apps, a.k.a AAD apps) are an essential component when interacting with Office 365 data outside of SharePoint – Mail, Calendar, Groups, etc.

As an O365 developer I have found myself writing JavaScript code against AAD apps (using ADAl.js) and often, especially during development, found myself entering a long list of Reply URLs. Reply URLs must be specified for any location from which authentication to AAD occurs. From a practical standpoint this results in someone (an Azure Administrator) having to update the list of Reply URLs every time a web part is inserted into a page or a new site is provisioned which relies on an Azure AD app.

If this is not done, the user is redirected to Azure login failure with ‘The reply address … does not match the reply addresses configured for the application’.

Error when Reply URL is not correctly specified
Error when Reply URL is not correctly specified

Perhaps the following is documented elsewhere but I have not come across it – a Reply URL can be specified using wildcards!

Using a wildcard Reply URL when configuring an AAD app
Using wildcard Reply URLs when configuring an AAD app

Probably the most common use for this is to end a Reply URL with an asterisk (wildcard) which will permit any URL which begins with the characters preceding it.

e.g. https://tenant.sharepoint.com/*
This example would support any URL coming from any page in SharePoint Online from within the named tenant.

It is also possible to use the wildcard character elsewhere in the Reply URL string.
e.g. https://*.sharepoint.com/*
This example would support any URL coming from any page in SharePoint Online from within *any* tenant.

Armed with this knowledge, be responsible and limit strictly how it is utilised. The implementation of Reply URL is a security feature and it is important that only trusted locations are allowed to interact with your app. I recommend only using wildcard Reply URLs in development environments.

Paul.