get token in event handler - mattchenderson/microsoft-identity-web GitHub Wiki

How to get an OBO token in an event handler or a long running process

Sometimes your web API will do long running processes on behalf of the user (think of OneDrive which creates albums for you). To achieve that, the idea is :

  • Before you start your long running process, store the access token that was used to call your web API in some kind of cache, so that you are able to retrieve it with a key.
  • Use this token to call the web API itself (in the example below a different controller after the callback

For instance, you could use a concept of LongRunningProcessContext:

using System;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Http;

namespace TodoListService
{
    public class LongRunningProcessContext : IDisposable
    {
        JwtSecurityToken _savedToken;
        HttpContext _httpContext;

        internal LongRunningProcessContext(HttpContext httpContext, JwtSecurityToken tokenToUse)
        {
            _httpContext = httpContext;
            _savedToken = httpContext.Items["JwtSecurityTokenUsedToCallWebAPI"] as JwtSecurityToken;
            httpContext.Items["JwtSecurityTokenUsedToCallWebAPI"] = tokenToUse;
        }

        public void Dispose()
        {
            _httpContext.Items["JwtSecurityTokenUsedToCallWebAPI"] = _savedToken;
        }
    }
}

To get this context, you can use a factory that enables you to store the access token used to call the web API, and gets you back a key (in this sample it's a GUID, but you could use the hash of the token, or anything you want). This key will need to be used to get back the token in your long running process. If that token has expired, of course, it won't be usable to call the web API (as the token validation won't allow it), but it will still be usable to call AcquireTokenOnBehalfOf, as a key. and AcquireTokenOnBehalfOf will refresh the OBO token as needed.

public interface ILongRunningProcessContextFactory
{
 string CreateKey(HttpContext http);
 LongRunningProcessContext UseKey(HttpContext httpContext, string key);
}
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace TodoListService
{
    /// <summary>
    /// This is a naïve implementation of a cache to save user assertions used
    /// to get an OBO token, in the case of long running processes which want to 
    /// call OBO and get the token refreshed.
    /// 
    /// TODO: document this better.
    /// Could use a IDistributed cache
    /// Needs to add 
    ///  services.AddSingleton<ILongRunningProcessAssertionCache, LongRunningProcessAssertionCache>();
    /// in the Startup.cs
    /// </summary>
    public class LongRunningProcessContextFactory : ILongRunningProcessContextFactory
    {
        /// <summary>
        /// Get a key associated with the current incoming token
        /// </summary>
        /// <param name="httpContext">Http context in which the controller action is running</param>
        /// <returns>A unique string repesenting the incoming token to the web API. This
        /// key will be used in the future to retrieve the incoming token even if it has expired therefore
        /// enabling getting an OBO token.</returns>
        public string CreateKey(HttpContext httpContext)
        {
            JwtSecurityToken token = httpContext.Items["JwtSecurityTokenUsedToCallWebAPI"] as JwtSecurityToken;
            string key = Guid.NewGuid().ToString();
            privateAssertionOfKey.TryAdd(key, token);
            return key;
        }

        /// <summary>
        /// Get a long running process context from a key
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public LongRunningProcessContext UseKey(HttpContext httpContext, string key)
        {
            JwtSecurityToken token = privateAssertionOfKey[key];
            return new LongRunningProcessContext(httpContext, token);
        }

        private IDictionary<string, JwtSecurityToken> privateAssertionOfKey = new Dictionary<string, JwtSecurityToken>();
    }
}

Here how you can inject the factory in the controllers by dependency injection

Controllers/HomeController.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.Resource;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using TodoListService.Models;

namespace TodoListService.Controllers
{
    [Authorize]
    [Route("api/[controller]")]
    [RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")]
    public class TodoListController : Controller
    {
        private readonly ITokenAcquisition _tokenAcquisition; // do not remove
        // The Web API will only accept tokens 1) for users, and 2) having the access_as_user scope for this API
        // In-memory TodoList
        private static readonly Dictionary<int, Todo> TodoStore = new Dictionary<int, Todo>();
        ILongRunningProcessContextFactory _longRunningProcessAssertionCache;

        public TodoListController(
            IHttpContextAccessor contextAccessor,
            ITokenAcquisition tokenAcquisition,
            ILongRunningProcessContextFactory longRunningProcessAssertionCache)
        {
            _tokenAcquisition = tokenAcquisition;
            _longRunningProcessAssertionCache = longRunningProcessAssertionCache;

            // Pre-populate with sample data
            if (TodoStore.Count == 0)
            {
                TodoStore.Add(1, new Todo() { Id = 1, Owner = $"{contextAccessor.HttpContext.User.Identity.Name}", Title = "Pick up groceries" });
                TodoStore.Add(2, new Todo() { Id = 2, Owner = $"{contextAccessor.HttpContext.User.Identity.Name}", Title = "Finish invoice report" });
            }
        }

        // GET: api/values
        // [RequiredScope("access_as_user")]
        [HttpGet]
        public async Task<IEnumerable<Todo>> GetAsync()
        {
            string owner = User.GetDisplayName();
            // Below is for testing multi-tenants
            var result = await _tokenAcquisition.GetAccessTokenForUserAsync(new string[] { "user.read" }).ConfigureAwait(false); // for testing OBO

            var result2 = await _tokenAcquisition.GetAccessTokenForUserAsync(new string[] { "user.read.all" },
                tokenAcquisitionOptions: new TokenAcquisitionOptions { ForceRefresh = true }).ConfigureAwait(false); // for testing OBO

            RegisterPeriodicCallbackForLongProcessing();

            // string token1 = await _tokenAcquisition.GetAccessTokenForUserAsync(new string[] { "user.read" }, "7f58f645-c190-4ce5-9de4-e2b7acd2a6ab").ConfigureAwait(false);
            // string token2 = await _tokenAcquisition.GetAccessTokenForUserAsync(new string[] { "user.read" }, "3ebb7dbb-24a5-4083-b60c-5a5977aabf3d").ConfigureAwait(false);

            await Task.FromResult(0); // fix CS1998 while the lines about the 2 tokens are commented out.
            return TodoStore.Values.Where(x => x.Owner == owner);
        }

The content of RegisterPeriodicCallbackForLongProcessing is the following. It uses the factory to store the token, and pass-in a key to the long running process (here simulated by a timer)

        /// <summary>
        /// This methods the processing of user data where the web API periodically checks the user
        /// date (think of OneDrive producing albums)
        /// </summary>
        private void RegisterPeriodicCallbackForLongProcessing()
        {
            // Get the token incoming to the web API - we could do better here.
            string key = _longRunningProcessAssertionCache.CreateKey(HttpContext);

            // Build the URL to the callback controller, based on the request.
            var request = HttpContext.Request;
            string url = request.Scheme + "://" + request.Host + request.Path.Value.Replace("todolist", "callback") + $"?key={key}";

            // Setup a timer so that the API calls back the callback every 10 mins.
            Timer timer = new Timer(async (state) =>
            {
                HttpClient httpClient = new HttpClient();
                
                var message = await httpClient.GetAsync(url);
            }, null, 1000, 1000 * 60 * 1);  // Callback every minute
        }

Here is the second controller: Controllers/CallbackController.cs, which uses the factory to retrieve the long running process context. In the scope of this context, the token usable by ITokenAcquisition, IDownstreamWebApi or the graph service client will be the right one for OBO to refresh the token.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.Resource;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using TodoListService.Models;

namespace TodoListService.Controllers
{
    [Authorize]
    [Route("api/[controller]")]
    //[RequiredScope("access_as_user")] 
    public class CallbackController : Controller
    {
        private readonly ITokenAcquisition _tokenAcquisition; 
        private ILogger _logger;
        ILongRunningProcessContextFactory _longRunningProcessAssertionCache;

        public CallbackController(
            IHttpContextAccessor contextAccessor,
            ITokenAcquisition tokenAcquisition,
            ILogger<CallbackController> logger,
            ILongRunningProcessContextFactory longRunningProcessAssertionCache)
        {
            _tokenAcquisition = tokenAcquisition;
            _logger = logger;
            _longRunningProcessAssertionCache = longRunningProcessAssertionCache;
        }


        // GET: api/values
        // [RequiredScope("access_as_user")]
        [HttpGet]
        [AllowAnonymous]
        public async Task GetAsync(string key)
        {
            var request = HttpContext.Request;
            string calledUrl = request.Scheme + "://" + request.Host + request.Path.Value + Request.QueryString;

            _logger.LogWarning($"{DateTime.UtcNow}: {calledUrl}");

            using (_longRunningProcessAssertionCache.UseKey(HttpContext, key))
            {
                var result = await _tokenAcquisition.GetAuthenticationResultForUserAsync(new string[] { "user.read" }).ConfigureAwait(false); // for testing OBO

                _logger.LogWarning($"OBO token acquired from {result.AuthenticationResultMetadata.TokenSource} expires {result.ExpiresOn.UtcDateTime}");


                // For breakpoint
                if (result.AuthenticationResultMetadata.TokenSource == Microsoft.Identity.Client.TokenSource.IdentityProvider)
                {
                }

            }
        }
    }
}

Sample

For a full sample, see https://github.com/AzureAD/microsoft-identity-web/tree/master/tests/WebAppCallsWebApiCallsGraph/TodoListService/Controllers

⚠️ **GitHub.com Fallback** ⚠️