Labwrights

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:

  • DefaultAzureCredential looks for an identity in a fixed order and uses the first one it finds. On your laptop, that's your az login session.
  • get_bearer_token_provider gets 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_provider replaces api_key. There's no key anywhere.
  • model is your deployment name. A deployment is your named instance of a model, so chat here 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's az login session 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_tokens is not supported. Switch to max_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.