Azure Function App and static AccessToken question

Carl McClellan 1 Reputation point
2022-08-17T18:19:34.34+00:00

Hello,

Setup:

My Project Configuration: In-Process, .NET Core 3.1, using Azure.Identity;

I have an Azure Function App that accesses an Azure Maps resource using a managed identity. My function app currently has only one function but I plan to add more, each of which will access the Azure Maps resource. I want to obtain an access token ONCE, and re-use this access token for any subsequent calls to Azure Maps from any of my functions (assuming the token is still valid). I've also paid close attention to 'Managing Connections in Azure Functions'. Microsoft recommends creating static variables to handle Http clients and other connection types to prevent socket exhaustion etc.. Instead of using static variables, I opted for Dependency Injection in my function app to achieve the desired goal. In doing so, I created Startup.cs in my project and it looks like this:

[assembly: FunctionsStartup(typeof(GeoSpatialFuncApp.Startup))]  
  
namespace GeoSpatialFuncApp  
{  
    class Startup : FunctionsStartup  
    {  
        public override void Configure(IFunctionsHostBuilder builder)  
        {  
            builder.Services.AddHttpClient();  
        }  
    }  
}  

The class that hosts my functions is not static (otherwise I could not achieve dependency injection). The wrapper class for my functions looks like this (up to and including the constructor):

namespace GeoSpatialFuncApp  
{  
    public class GeoSpatial  
    {  
        //Logging  
  
        private readonly ILogger<GeoSpatial> _logger;                             
        private bool _logIdentity = false;  
          
        //Azure Maps  
  
        private readonly HttpClient _azureMapClient;            
        private readonly string _baseUrl = "https://atlas.microsoft.com";  
        private readonly string _base_path = "/";  
        private string _azureMapsUserAssignedClientId;  
        private string _azureMapsClientId;  
  
        //Identity  
  
        private readonly DefaultAzureCredential _credentials;  
        private AccessToken _azureMapsToken;  
          
        public GeoSpatial(ILogger<GeoSpatial> log, IHttpClientFactory httpClientFactory)  
        {  
            _logger = log;  
  
             //use a single instance to prevent socket exhaustion  

            _azureMapClient = httpClientFactory.CreateClient();  
  
            _azureMapClient.DefaultRequestHeaders.Accept.Clear();  
            _azureMapClient.DefaultRequestHeaders.Accept.Add(new   
            MediaTypeWithQualityHeaderValue("application/json"));  
  
            _azureMapsUserAssignedClientId = Environment.GetEnvironmentVariable("APP_AZURE_MAP_IDENTITY");  
            _logIdentity = Convert.ToBoolean(Environment.GetEnvironmentVariable("APP_LOG_USER_IDENTITY"));  
            _azureMapsClientId = Environment.GetEnvironmentVariable("APP_AZURE_MAP_CLIENT_ID" );  
  
            _credentials = new DefaultAzureCredential(  
                new DefaultAzureCredentialOptions  
                {  
                    ExcludeEnvironmentCredential = true,  
                    ExcludeManagedIdentityCredential = false,  
                    ExcludeSharedTokenCacheCredential = true,  
                    ExcludeVisualStudioCredential = false,  
                    ExcludeVisualStudioCodeCredential = true,  
                    ExcludeAzureCliCredential = true,  
                    ExcludeInteractiveBrowserCredential = true,  
                    ManagedIdentityClientId = _azureMapsUserAssignedClientId  
                });  
  
            RefreshAzureMapToken();      
        }  

...

The Problem

Fortunately, when I debug my app locally or test it in a deployed environment I'm getting the same behavior, which is as follows:

  1. Startup.cs is called ONCE, per debug session or host lifetime (ok, this is good)
  2. The class that wraps my functions is created every time I invoke a function (verified by setting breakpoints in the constructor). This means that my class variables (i.e. token, credential, environment variables) are lost every time a function completes execution. Clearly this is not what I want because getting a token for Azure Maps is time consuming and I don't want to do this for every function call. I also don't want to have to re-read settings or application variables every time a function runs.

The Solution/ Open Questions

  1. If I make the AccessToken class variable static then the value DOES persist between function invocations, even though the constructor for the function class is always invoked. This kind-of solves the problem, and I suppose I could make all of my class members static, but then I would have to write logic to see if the values have been set or not (so I don't re-read environment variables that have already been set or re-acquire tokens that have already been acquired or re-create credentials that have already been created).
  2. With regards to AccessToken (which is a struct), I do not understand why this object only exposes the token and the expiration timestamp. Where is my refresh token? Microsoft documentation clearly states that if you use DefaultAzureCredential.GetToken() then I AM responsible for token caching and refresh. I'm completely unsure how to do this using Azure.Identity client library. In order to get around this, I created a function that checks expiration timestamp of the token and re-acquires if the token is within 3 minutes of expiring:
    private void RefreshAzureMapToken(bool force = false)  
            {  
                try  
                {  
                    _logger.LogInformation($"RefreshAzureMapToken Invoked");  
    
                    if (!force)  
                    {  
                        TimeSpan dif = _azureMapsToken.ExpiresOn - DateTimeOffset.UtcNow;  
    
                        if (dif.TotalMinutes > 3)  
                            return;  
                    }  
    
                    _azureMapsToken = _credentials.GetToken(new TokenRequestContext(new[] { "https://atlas.microsoft.com/.default" }));  
    
                }  
                catch (AuthenticationFailedException e)  
                {  
                    Console.WriteLine($"Authentication Failed. {e.Message}");  
                }  
                 catch (Exception e)  
                {  
                    Console.WriteLine($"Authentication Failed - General Exception. {e.Message}");  
                }  
    
            }  
    

However, if I have multiple functions running simultaneously when a token refresh is necessary then I can see conflicts arising - do I need to put synchronization constructs around the token acquisition itself, or is DefaultAzureCredential doing the token caching/refreshing for me automatically in a thread safe manner (which would be contrary to what the documentation says).

Lastly, would it be a better solution if I created my own static service class that has all of these variables in it which I initialize in Startup.cs and inject into my function class in the same way the logger and HttpClient is getting injected? Even if I did this, that wouldn't necessarily solve the problem of concurrent access to the token.

Thank you, I hope this was clearly explained.

Azure Functions
Azure Functions
An Azure service that provides an event-driven serverless compute platform.
5,911 questions
Microsoft Security Microsoft Entra Microsoft Entra ID
{count} votes

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.