Guides · 4 min read · October 26, 2026
Azure OpenAI without API keys: keyless sign-in from Python
Most quickstarts begin with an API key pasted into an environment variable. It works, and it's the first thing a security review flags. This guide shows the keyless way: your code signs in with an identity, gets a short-lived token, and sends that instead. Nothing secret lives in your code, your config, or your repo.
The code below is the same code that ran against live Azure in October 2026, in the RAG system linked at the end.
Why keys are a problem
An API key is like a house key. Whoever holds a copy gets in, and you can't tell who used it. Keys leak through commits, logs, screenshots, and chat messages. When one leaks, you rotate it, and then update every place that used it.
Keyless sign-in is like a hotel key card. It's issued to a named guest, it opens only certain doors, and it stops working after a set time. In Azure terms:
- Microsoft Entra ID knows who you are, and who each app is.
- A role assignment gives one identity one role, on one resource.
- Your code asks Entra ID for a token that lasts about an hour, and sends it with each request.
How the chain works
az login (you, once)
↓
DefaultAzureCredential finds your identity
↓
Entra ID issues a short-lived token for Azure AI services
↓
Azure OpenAI checks the token, then checks your role
The role that matters is Cognitive Services OpenAI User. It lets an identity call models, and nothing else. It can't create deployments, read keys, or delete anything.
Step 1: give your identity the role
Sign in with the Azure CLI, then assign the role on your Azure OpenAI resource. Replace the two placeholders with your own resource names.
az login
ME=$(az ad signed-in-user show --query id -o tsv)
OAI=$(az cognitiveservices account show \
--name <your-openai-resource> --resource-group <your-resource-group> \
--query id -o tsv)
az role assignment create \
--assignee-object-id "$ME" --assignee-principal-type User \
--role "Cognitive Services OpenAI User" \
--scope "$OAI"
The scope is the single resource, not the whole subscription. That's the point: the narrowest access that works.
In Terraform, the same thing is one azurerm_role_assignment with role_definition_name = "Cognitive Services OpenAI User", scoped to the account's ID.
Step 2: the code
Install the two libraries:
pip install openai azure-identity
Then:
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
from openai import AzureOpenAI
token_provider = get_bearer_token_provider(
DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default"
)
client = AzureOpenAI(
azure_endpoint="https://<your-openai-resource>.openai.azure.com/",
azure_ad_token_provider=token_provider,
api_version="2024-10-21",
)
response = client.chat.completions.create(
model="chat", # your DEPLOYMENT name, not the model's name
messages=[{"role": "user", "content": "Summarize RAG in one sentence."}],
max_completion_tokens=500,
)
print(response.choices[0].message.content)
What each part does:
DefaultAzureCredentiallooks for an identity in a fixed order and uses the first one it finds. On your laptop, that's youraz loginsession.get_bearer_token_providergets a token and refreshes it before it expires. The URL is the token's scope: it says the token is only for Azure AI services.azure_ad_token_providerreplacesapi_key. There's no key anywhere.modelis your deployment name. A deployment is your named instance of a model, sochathere is whatever you called yours.
Newer models, such as the gpt-5 family, take max_completion_tokens; they reject max_tokens. They also run only at their default temperature. The budget covers the model's internal reasoning tokens too, so set it larger than the answer you expect.
The same code, everywhere
The best part of DefaultAzureCredential is that the code never changes. Only the identity it finds does:
| Where the code runs | Identity it uses |
|---|---|
| Your laptop | Your az login session |
| An Azure service, such as Container Apps | That service's managed identity |
| CI, such as GitHub Actions | A federated credential, with no secret stored |
Give each identity the same role on the same resource, and it works.
Errors you'll hit
- 401 or PermissionDenied, just after assigning the role. Role assignments take a few minutes to reach every part of Azure. Wait about five minutes and try again.
- "DefaultAzureCredential failed to retrieve a token". The terminal isn't signed in, so run
az login. Or your code runs inside a Docker container, where your laptop'saz loginsession doesn't exist. Run it on your machine, or give the container its own identity. - The deployment doesn't exist. You passed the model's name, such as a gpt version, instead of your deployment's name.
max_tokensis not supported. Switch tomax_completion_tokens, as above.
Then turn keys off
Once nothing uses keys, switch them off on the resource. In Terraform, set local_auth_enabled = false on the azurerm_cognitive_account. After that, a leaked key is worthless, because the service won't accept keys at all.
Azure AI Search and the other AI services sign in the same way, each with its own role. In the package below, every call in a full RAG system runs keyless: embeddings, search, OCR, and answers.