Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/azure/TOC.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@
href: ./sdk/authentication/additional-methods.md
- name: Credential chains
href: ./sdk/authentication/credential-chains.md
- name: Azure Identity library best practices
href: ./sdk/authentication/authentication-best-practices.md
- name: ASP.NET Core guidance
href: ./sdk/aspnetcore-guidance.md
- name: Resource management
Expand Down
45 changes: 45 additions & 0 deletions docs/azure/sdk/authentication/authentication-best-practices.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
title: Authentication best practices with the Azure Identity library for .NET
description: This article describes authentication best practices to follow when using the Azure Identity library.
ms.topic: conceptual
ms.date: 01/15/2025
---

# Authentication best practices with the Azure Identity library for .NET

This article offers guidelines to help you maximize the performance and reliability of your .NET apps when authenticating to Azure services. To make the most of the Azure Identity library for .NET, it's important to understand potential issues and mitigation techniques.

## Use deterministic credentials in production environments

[!INCLUDE [default-azure-credential-usage](../includes/default-azure-credential-usage.md)]

## Reuse credential instances

Reuse credential instances when possible to improve app resilience and reduce the number of access token requests issued to Microsoft Entra ID. When a credential is reused, an attempt is made to fetch a token from the app token cache managed by the underlying MSAL dependency. For more information, see [Token caching in the Azure Identity client library](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/samples/TokenCache.md).

> [!NOTE]
> A high-volume app that doesn't reuse credentials may encounter HTTP 429 throttling responses from Microsoft Entra ID, which can lead to app outages.
In an ASP.NET Core app, implement credential reuse through the `UseCredential` method of `Microsoft.Extensions.Azure`:

:::code language="csharp" source="../snippets/auth-best-practices/Program.cs" id="snippet_credential_reuse_Dac" highlight="6,7" :::

For information on this approach, see [Authenticate using Microsoft Entra ID](/dotnet/azure/sdk/aspnetcore-guidance?tabs=api#authenticate-using-microsoft-entra-id).

Other types of .NET apps can reuse credential instances as follows:

:::code language="csharp" source="../snippets/auth-best-practices/Program.cs" id="snippet_credential_reuse_noDac" highlight="8, 12" :::

## Understand the managed identity retry strategy

The Azure Identity library for .NET allows you to authenticate via managed identity with `ManagedIdentityCredential`. The way you use `ManagedIdentityCredential` impacts the applied retry strategy:

- When used via `DefaultAzureCredential`:
- No retries are attempted when token acquisition fails, which makes this the least resilient option.
- When used via any other approach, such as `ChainedTokenCredential` or `ManagedIdentityCredential` directly:
- The time interval between retries starts at 0.8 seconds, and a maximum of five retries are attempted.
- If the Azure service to which you're authenticating provides a `Retry-After` response header, the next retry is delayed by the duration specified in that header's value.
- If the service doesn't provide a `Retry-After` header, the maximum permissible delay between retries is 1 minute.
- To change any of the default retry settings, use the `Retry` property on `ManagedIdentityCredentialOptions`. For example, retry a maximum of three times, with a starting interval of 0.5 seconds:

:::code language="csharp" source="../snippets/auth-best-practices/Program.cs" id="snippet_retries" highlight="5-9":::
11 changes: 1 addition & 10 deletions docs/azure/sdk/authentication/credential-chains.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,7 @@ The preceding code sample creates a tailored credential chain comprised of two c

## Usage guidance for DefaultAzureCredential

`DefaultAzureCredential` is undoubtedly the easiest way to get started with the Azure Identity library, but with that convenience comes tradeoffs. Once you deploy your app to Azure, you should understand the app's authentication requirements. For that reason, strongly consider moving from `DefaultAzureCredential` to one of the following solutions:

- A specific `TokenCredential` implementation, such as `ManagedIdentityCredential`. See the [**Derived** list](/dotnet/api/azure.core.tokencredential?view=azure-dotnet&preserve-view=true#definition) for options.
- A pared-down `ChainedTokenCredential` implementation optimized for the Azure environment in which your app runs.

Here's why:

- **Debugging challenges**: When authentication fails, it can be challenging to debug and identify the offending credential. You must enable logging to see the progression from one credential to the next and the success/failure status of each. For more information, see [Debug a chained credential](#debug-a-chained-credential).
- **Performance overhead**: The process of sequentially trying multiple credentials can introduce performance overhead. For example, when running on a local development machine, managed identity is unavailable. Consequently, `ManagedIdentityCredential` always fails in the local development environment, unless explicitly disabled via its corresponding `Exclude`-prefixed property.
- **Unpredictable behavior**: `DefaultAzureCredential` checks for the presence of certain [environment variables][env-vars]. It's possible that someone could add or modify these environment variables at the system level on the host machine. Those changes apply globally and therefore alter the behavior of `DefaultAzureCredential` at runtime in any app running on that machine.
[!INCLUDE [default-azure-credential-usage](../includes/default-azure-credential-usage.md)]

## Debug a chained credential

Expand Down
31 changes: 31 additions & 0 deletions docs/azure/sdk/includes/default-azure-credential-usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

`DefaultAzureCredential` is the most approachable way to get started with the Azure Identity library, but that convenience also introduces certain tradeoffs. For example, the specific credential in the [chain](/dotnet/api/azure.identity.defaultazurecredential?view=azure-dotnet) that will succeed and be used for request authentication can't be guaranteed ahead of time. In a production environment, this unpredictability can introduce significant and sometimes subtle problems.

For example, consider the following hypothetical sequence of events:

1. An organization's security team mandates all apps use managed identity to authenticate to Azure resources.
1. For months, a .NET app hosted on an Azure Virtual Machine (VM) successfully uses `DefaultAzureCredential` to authenticate via managed identity.
1. Without telling the support team, a developer installs the Azure CLI on that VM and runs the `az login` command to sign-in to Azure.
1. Due to a separate configuration change in the Azure environment, authentication via the original managed identity unexpectedly begins to fail silently.
1. `DefaultAzureCredential` skips the failed `ManagedIdentityCredential` and searches for the next available credential, which is the Azure CLI credentials.
1. The team is unaware of the hidden authentication failure because logging is disabled by default.

`DefaultAzureCredential` also introduces the following challenges in some scenarios:

- **Debugging challenges**: When authentication fails, it can be difficult to identify and [debug the offending credential](/dotnet/azure/sdk/authentication/credential-chains?tabs=dac#debug-a-chained-credential). Enable logging to see the sequential progression and success or failure status of each credential.
- **Performance overhead**: Sequential credential attempts can introduce performance overhead. For example, managed identity is unavailable on a local development machine. Consequently, `ManagedIdentityCredential` always fails locally, unless explicitly disabled via its corresponding `Exclude`-prefixed property.

To prevent these types of subtle issues or silent failures in production apps, strongly consider moving from `DefaultAzureCredential` to one of the following deterministic solutions:

- A specific `TokenCredential` implementation, such as `ManagedIdentityCredential`. See the [**Derived** list](/dotnet/api/azure.core.tokencredential?view=azure-dotnet&preserve-view=true#definition) for options.
- A pared-down `ChainedTokenCredential` implementation optimized for the Azure environment in which your app runs. `ChainedTokenCredential` essentially creates a specific allow-list of acceptable credential options, such as `ManagedIdentity` for production and `VisualStudioCredential` for development.

For example, consider the following `DefaultAzureCredential` configuration:

:::code language="csharp" source="../snippets/authentication/credential-chains/Program.cs" id="snippet_Dac" highlight="6,7":::

Replace the preceding code with the following `ChainedTokenCredential` implementation to intentionally specify your desired credentials:

:::code language="csharp" source="../snippets/authentication/credential-chains/Program.cs" id="snippet_Ctc" highlight="6-8":::

In this example, `ManagedIdentityCredential` would be automatically discovered in production, while `VisualStudioCredential` would work in local development environments.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.13.1" />
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.18.2" />
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.7.0" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.23.0" />
<PackageReference Include="Microsoft.Extensions.Azure" Version="1.9.0" />
</ItemGroup>

</Project>
82 changes: 82 additions & 0 deletions docs/azure/sdk/snippets/auth-best-practices/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Azure.Storage.Blobs;
using Microsoft.Extensions.Azure;

var userAssignedClientId = "<user-assigned-client-id>";
var builder = WebApplication.CreateBuilder(args);

#region snippet_credential_reuse_Dac
builder.Services.AddAzureClients(clientBuilder =>
{
clientBuilder.AddSecretClient(new Uri("<key_vault_url>"));
clientBuilder.AddBlobServiceClient(new Uri("<storage_url>"));

DefaultAzureCredential credential = new();
clientBuilder.UseCredential(credential);
});
#endregion snippet_credential_reuse_Dac

#region snippet_credential_reuse_noDac
ChainedTokenCredential credentialChain = new(
new ManagedIdentityCredential(
ManagedIdentityId.FromUserAssignedClientId(userAssignedClientId)),
new VisualStudioCredential());

BlobServiceClient blobServiceClient = new(
new Uri("<blob-storage-uri>"),
credentialChain);

SecretClient secretClient = new(
new Uri("<key_vault_url>"),
credentialChain);
#endregion snippet_credential_reuse_noDac

#region snippet_retries
ManagedIdentityCredentialOptions miCredentialOptions = new(
ManagedIdentityId.FromUserAssignedClientId(userAssignedClientId)
)
{
Retry =
{
MaxRetries = 3,
Delay = TimeSpan.FromSeconds(0.5),
}
};
ChainedTokenCredential tokenChain = new(
new ManagedIdentityCredential(miCredentialOptions),
new VisualStudioCredential()
);
#endregion

builder.Services.AddEndpointsApiExplorer();

var app = builder.Build();

app.UseHttpsRedirection();

var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");

app.Run();

internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"profiles": {
"AuthBestPractices": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:56902;http://localhost:56903"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions docs/azure/sdk/snippets/auth-best-practices/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Loading