Speeding up DefaultAzureCredential for local development

If you’ve ever found yourself waiting for your local .NET application to authenticate with Azure services, you’re not alone. That delay is often caused by DefaultAzureCredential trying authentication methods that simply don’t exist on your development machine.

Why Your Local Development Is Slow

When you use DefaultAzureCredential without any customization, it tries a whole series of authentication methods one after another until something works. For local development, this is painfully inefficient.

Think about it: your development machine doesn’t have managed identities or workload identities – those only exist in Azure environments. Yet DefaultAzureCredential will still stubbornly try to use them, waiting for timeouts before giving up and moving to the next method. Those wasted seconds add up, especially when you’re frequently restarting your application during development.

As shown in Microsoft’s documentation on credential chains, DefaultAzureCredential tries these credential types in order:

  1. Environment
  2. Workload Identity
  3. Managed Identity
  4. Visual Studio
  5. Azure CLI
  6. Azure PowerShell
  7. Azure Developer CLI
  8. Interactive browser (when explicitly enabled)

The first three are the biggest culprits for slowing down local development.

Diagram showing the Azure authentication flow between a .NET app, Credential chain, and TokenCredential instance. The flow shows six numbered steps: 1) .NET app authenticates to Microsoft Entra ID, 2) Credential chain gets token, 3) Inside a loop, it fetches token from TokenCredential instance, 4) TokenCredential gets token, 5) Result is returned when an AccessToken is received, 6) AccessToken is returned to the .NET app. The diagram illustrates how the credential chain traverses the TokenCredential collection until a valid access token is received.

The Simple Fix: Tell It What to Skip

The good news is that fixing this is easy. You don’t need to abandon DefaultAzureCredential entirely, you just need to tell it which methods to skip when you’re developing locally:

var isDevelopment = builder.Environment.IsDevelopment();
TokenCredential credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions
{
    ExcludeEnvironmentCredential = isDevelopment,
    ExcludeManagedIdentityCredential = isDevelopment,
    ExcludeWorkloadIdentityCredential = isDevelopment,
});

This skips the 3 first parts of the flow, directly trying a development credential.

Integration with HttpClient

Here’s how you can integrate this optimized credential with an HttpClient used to call an API:

builder.Services.AddTransient<MyApiClientClientBearerTokenHandler>();

builder.Services.AddHttpClient("MyApiClient", httpClient =>
{
    httpClient.BaseAddress = new Uri(builder.Configuration["MyApiClient:BaseUrl"] ??
                                     throw new Exception("Missing configuration 'MyApiClient:BaseUrl'"));
})
    .AddHttpMessageHandler<MyApiClientClientBearerTokenHandler>();

The handler will take care of adding the authentication token to your requests:

public sealed class MyApiClientClientBearerTokenHandler(ITokenAcquisition tokenAcquisition, IConfiguration configuration)
    : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var token = await GetUserTokenAsync();
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        return await base.SendAsync(request, cancellationToken);
    }

    private async Task<string> GetUserTokenAsync()
    {
        var s = configuration["MyApiClientClient:Scope"];
        return await tokenAcquisition.GetAccessTokenForUserAsync([
            s ?? throw new Exception("Missing configuration 'MyApiClientClient:Scope'")
        ]);
    }
}

Putting It All Together

Here’s how you might use this optimized credential with your HTTP clients:

builder.Services.AddTransient&lt;MyApiClientClientBearerTokenHandler>();

builder.Services.AddHttpClient("MyApiClient", httpClient =>
{
    httpClient.BaseAddress = new Uri(builder.Configuration["MyApiClient:BaseUrl"] ??
                                     throw new Exception("Missing configuration 'MyApiClient:BaseUrl'"));
})
    .AddHttpMessageHandler&lt;MyApiClientClientBearerTokenHandler>();

And the handler to attach the token:

public sealed class MyApiClientClientBearerTokenHandler(ITokenAcquisition tokenAcquisition, IConfiguration configuration)
    : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var token = await GetUserTokenAsync();
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        return await base.SendAsync(request, cancellationToken);
    }

    private async Task<string> GetUserTokenAsync()
    {
        var s = configuration["MyApiClientClient:Scope"];
        return await tokenAcquisition.GetAccessTokenForUserAsync([
            s ?? throw new Exception("Missing configuration 'MyApiClientClient:Scope'")
        ]);
    }
}

Things to Remember

  • Only skip these credentials in development. Your production code should still use the full chain.
  • The beauty of this approach is that it’s environment-aware – your app will automatically use the right authentication strategy based on where it’s running.
  • If your development process involves environment variables for authentication, you might want to keep EnvironmentCredential enabled.

Wrap Up

I have been experimenting with keeping app start times fast. Especially when you need to access a service like Azure Key Vault, or Azure App Config during application startup, then you want the authentication to be fast.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *