DefaultAzureCredential and the credential chain (TypeScript)
How @azure/identity picks an identity — the exact DefaultAzureCredential order (Environment, Workload Identity, Managed Identity, VS Code, Azure CLI, PowerShell, azd, broker), the environment variables each step reads, AZURE_TOKEN_CREDENTIALS to pin one, the AZURE_CLIENT_ID trap for managed identities, which credential to use in production, ChainedTokenCredential, and logging which identity actually signed in.
markdownreferencev1KFB7-TGY9stage: buildtrust: officialservice: azure-identityinterface: sdkDefaultAzureCredentialcredential chain orderChainedTokenCredentialManagedIdentityCredential clientIdEnvironmentCredentialWorkloadIdentityCredentialAzureCliCredentialAZURE_TOKEN_CREDENTIALSAZURE_CLIENT_IDAZURE_TENANT_IDAZURE_CLIENT_SECRETAZURE_CLIENT_CERTIFICATE_PATHAZURE_FEDERATED_TOKEN_FILEmanagedIdentityClientIdrequiredEnvVarsallowLoggingAccountIdentifiersAZURE_LOG_LEVELwrong identity@azure/identity 4.14
DefaultAzureCredential and the credential chain
DefaultAzureCredential tries credentials in a fixed order and uses the first one that returns a token, so "it works on my machine" usually means your Azure CLI login answered, and "403 in Azure" usually means a different identity answered than the one you assigned the role to. Read the order, then log which account signed in.
The order (@azure/identity 4.14.0)
| # | Credential | Answers when |
|---|---|---|
| 1 | EnvironmentCredential | AZURE_TENANT_ID + AZURE_CLIENT_ID + AZURE_CLIENT_SECRET, or AZURE_CLIENT_CERTIFICATE_PATH (PEM with private key; pfx isn't supported). Secret wins if both are set |
| 2 | WorkloadIdentityCredential | AZURE_FEDERATED_TOKEN_FILE + a client ID + AZURE_TENANT_ID (Kubernetes workload identity) |
| 3 | ManagedIdentityCredential | The host provides a managed identity (App Service, Functions, Container Apps, VM, AKS…) |
| 4 | VisualStudioCodeCredential | Signed in through the VS Code Azure Resources extension (needs @azure/identity-vscode) |
| 5 | AzureCliCredential | az login has a session |
| 6 | AzurePowerShellCredential | Connect-AzAccount has a session |
| 7 | AzureDeveloperCliCredential | azd auth login has a session |
| 8 | Broker credential | @azure/identity-broker is installed (Windows account broker) |
Continuation: since 3.3.0, the developer credentials (4–7) are all tried even if one errors. A deployed credential (2–3) that attempts a token and fails stops the chain with an exception, so a broken managed identity doesn't silently fall through to something else.
The AZURE_CLIENT_ID trap
For steps 2 and 3 the client ID is managedIdentityClientId (option) or the AZURE_CLIENT_ID environment variable. Set AZURE_CLIENT_ID for a service principal but forget the secret, and step 1 is skipped while step 3 asks the host for a user-assigned managed identity with that client ID. On a VM, that is the IMDS answer The requested identity hasn't been assigned to this resource. (the hub's reading of the two code paths). Set the variable only to the identity you mean.
Pin the credential in production
AZURE_TOKEN_CREDENTIALS narrows the chain: prod (steps 1–3), dev (steps 4–8), or one name: EnvironmentCredential, WorkloadIdentityCredential, ManagedIdentityCredential, VisualStudioCodeCredential, AzureCliCredential, AzurePowerShellCredential, AzureDeveloperCliCredential. An unknown value throws. Add requiredEnvVars so a missing variable fails at startup instead of falling back to the whole chain. In code where only one identity is right, use that credential class directly:
import {
ChainedTokenCredential,
DefaultAzureCredential,
ManagedIdentityCredential,
AzureCliCredential,
} from "@azure/identity";
// Production on Azure: exactly this user-assigned identity, nothing else.
export const prodCredential = new ManagedIdentityCredential({
clientId: process.env.APP_IDENTITY_CLIENT_ID ?? "",
});
// One code path for laptop and Azure, with an explicit order.
export const appCredential = new ChainedTokenCredential(prodCredential, new AzureCliCredential());
// Or keep DefaultAzureCredential but fail fast if it isn't pinned.
export const pinned = new DefaultAzureCredential({
requiredEnvVars: ["AZURE_TOKEN_CREDENTIALS"],
managedIdentityClientId: process.env.APP_IDENTITY_CLIENT_ID,
loggingOptions: { allowLoggingAccountIdentifiers: true },
});
The JavaScript library has no exclude…Credential options (other languages do): build a ChainedTokenCredential instead.
See which identity answered
import { setLogLevel } from "@azure/logger";
setLogLevel("info"); // or AZURE_LOG_LEVEL=info
With loggingOptions.allowLoggingAccountIdentifiers: true, a successful sign-in logs [Authenticated account] Client ID: … Tenant ID: … User Principal Name: … Object ID (user): …. Compare that object ID with the role assignment's principal. Logs carry sensitive data: don't ship them at info in production.
Tokens and scopes
A credential returns a token for a scope, <resource>/.default. The SDK clients pass the right one for you; you need it only for raw REST calls (credential.getToken("https://management.azure.com/.default")). Resource Manager is https://management.azure.com/.default; Key Vault is https://vault.azure.net/.default. Each service family in this hub gives its own scope.
Managed identity tokens are valid for 24 hours and cached by the platform, so a role or group change can take hours to reach a managed identity: see reference/managed-identity/errors.md.
Next
Error messages from each credential: reference/azure-identity/errors.md. Setting up a managed identity: guides/managed-identity/use-managed-identity.md.