Skip to content

feat: add ability to auth to azure with token - #21764

Closed
Harshit28j wants to merge 4 commits into
BerriAI:mainfrom
Harshit28j:litellm_feat_azure_auth_handle
Closed

feat: add ability to auth to azure with token#21764
Harshit28j wants to merge 4 commits into
BerriAI:mainfrom
Harshit28j:litellm_feat_azure_auth_handle

Conversation

@Harshit28j

@Harshit28j Harshit28j commented Feb 21, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Type

🆕 New Feature
📖 Documentation
✅ Test

Changes

image
  • Core Proxy Cache Update: Brought parity to Azure Managed Redis caches with existing GCP IAM auth. Allows LiteLLM to auto-authenticate with Azure AD (Entra ID) via temporary, rotating token injection without relying on static connection passwords.
  • Auto-Refresh Handlers: Created create_azure_ad_redis_connect_func hook which utilizes the azure-identity library to extract fresh OAuth access tokens mapped specifically for https://redis.azure.com/.default scopes.
  • Coverage: Implemented robust mocking validation inside tests/test_litellm/test_utils.py to ensure credentials parsing and fallback pathways correctly build their injection handlers against real Client IDs, Secrets, and system-assigned behaviors.
  • End-to-end Validated: Verified E2E with mock Azure Identity token generation communicating directly to a mock standalone redis Docker image.
  • Documentation: New dedicated doc section added covering proper .yaml configuration usage (azure_redis_ad_token: true) mapped over azure_client_id specifications.

E2E Verification Details

  • Redis Isolation: Started a password-protected Docker Redis to force a strict authentication requirement.
  • SDK Mocking: Intercepted the azure-identity library to return our local Redis password as a fake AD token.
  • Handshake Validation: Verified LiteLLM correctly requested the token and successfully executed the AUTH <token> command.
  • Connectivity Proof: Confirmed successful cache reads/writes by achieving a ~1.8ms response time and observing the x-litellm-cache-key header.

@vercel

vercel Bot commented Feb 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Feb 21, 2026 3:16pm

Request Review

@greptile-apps

greptile-apps Bot commented Feb 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds passwordless Azure AD (Entra ID) authentication for Redis in LiteLLM, mirroring the existing GCP IAM auth pattern. It introduces _build_azure_credential, create_azure_ad_redis_connect_func (sync path, credential reused across connections), and _generate_azure_ad_redis_token (one-shot for async paths), and correctly cleans up Azure-specific kwargs before they reach redis.Redis().

Key observations:

  • The sync path correctly builds the credential once and reuses it across reconnects, enabling transparent token refresh via the Azure SDK.
  • The async cluster, standard async, and connection pool paths all fetch a one-shot token at client-creation time — an inherent limitation of the async Redis client not supporting redis_connect_func. The cluster path documents this; the connection pool path is missing the same warning.
  • _azure_client_secret (a raw credential value) is stored as a plain attribute on redis_connect_func for use by the async cluster path, which is a potential exposure risk through debug logging or introspection.
  • test_redis_client_logic_azure_ad_auth calls _get_redis_client_logic with the Azure AD flag enabled without mocking azure.identity; since _build_azure_credential imports eagerly, this test will raise ImportError in CI environments where azure-identity is not installed.

Confidence Score: 3/5

  • Safe to review further; core logic is sound but there is a CI reliability issue in the tests and a credential exposure concern in the async cluster path.
  • The sync auth path is well-implemented and follows the established GCP pattern. However, test_redis_client_logic_azure_ad_auth lacks the azure.identity mock needed to run reliably in CI without the optional package, a raw credential attribute is stored on a public function object, and the connection pool token-expiry caveat is not documented. None of these are blocking correctness for the happy path, but they represent reliability and security gaps that should be addressed before merge.
  • litellm/_redis.py (credential attribute exposure, async token expiry for connection pool) and tests/test_litellm/test_utils.py (test_redis_client_logic_azure_ad_auth missing azure.identity mock)

Important Files Changed

Filename Overview
litellm/_redis.py Core implementation of Azure AD Redis auth — adds _build_azure_credential, create_azure_ad_redis_connect_func, and _generate_azure_ad_redis_token; correctly removes Azure kwargs from redis_kwargs; stores raw client credential value on function object (style concern); async cluster/pool/standard-async paths all fetch a one-shot token that will expire without a refresh path.
tests/test_litellm/test_utils.py New Azure AD Redis tests added; most tests correctly use patch.dict("sys.modules", ...) for isolation, but test_redis_client_logic_azure_ad_auth calls _get_redis_client_logic with Azure AD enabled without mocking azure.identity, causing it to fail if azure-identity is not installed in CI.
docs/my-website/docs/caching/azure_redis_passwordless.md New documentation page covering all three Azure auth modes (system-assigned managed identity, user-assigned managed identity, service principal); accurate and covers SSL requirement, prerequisites, and how the token rotation works.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[azure_redis_ad_token enabled] --> B[_get_redis_client_logic]
    B --> C[_build_azure_credential]
    C --> D{Which auth mode?}
    D -->|Service Principal env vars| E[ClientSecretCredential]
    D -->|User-Assigned Identity| F[ManagedIdentityCredential]
    D -->|System-Assigned Identity| G[DefaultAzureCredential]
    E & F & G --> H[Reusable credential object]

    H --> I{Client type}

    I -->|Sync redis.Redis| J[create_azure_ad_redis_connect_func]
    J --> K[ad_connect closure - credential reused]
    K --> L[Called per connection/reconnect]
    L --> M[get_token on each connect]
    M --> N[AUTH with fresh token - auto-renewed]

    I -->|Async RedisCluster| O[_generate_azure_ad_redis_token]
    I -->|Async Redis standard| O
    I -->|Async ConnectionPool| O
    O --> P[One-shot fetch at creation time]
    P --> Q[Set as static password]
    Q --> R[Expires in approx 1 hour - no refresh path]
Loading

Last reviewed commit: 32877e0

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment thread tests/test_litellm/test_utils.py Outdated
Comment thread tests/test_litellm/test_utils.py Outdated
Comment thread litellm/_redis.py Outdated
@giulio-leone

Copy link
Copy Markdown
Contributor

Automated patch bundle from next-100 unresolved backlog expansion.\nGenerated due limited direct branch-write access; please apply/cherry-pick minimal edits below.\n\n## PR #21764litellm_feat_azure_auth_handle (3 unresolved)

Unresolved thread summary

  • T1 tests/test_litellm/test_utils.py:2465 — Test will fail if azure-identity is not installed
  • T2 tests/test_litellm/test_utils.py:2486 — Same mocking issue for ClientSecretCredential
  • T3 litellm/_redis.py:388 — Azure AD silently overwrites GCP IAM if both configured

Minimal patch proposals

  • T1 tests/test_litellm/test_utils.py:2465
    • Edit steps:
      1. Adjust test inputs/assertions to validate the reviewer-reported behavior and prevent regressions.
      2. Keep the test deterministic and verify it fails before / passes after the patch intent.
  • T2 tests/test_litellm/test_utils.py:2486
    • Edit steps:
      1. Adjust test inputs/assertions to validate the reviewer-reported behavior and prevent regressions.
      2. Keep the test deterministic and verify it fails before / passes after the patch intent.
  • T3 litellm/_redis.py:388
    • Edit steps:
      1. Replace silent fallback with explicit validation error for unsupported/invalid values.
      2. Add or update one focused regression test near this module for the corrected behavior.

Comment on lines +2793 to +2809
def test_generate_azure_ad_redis_token_import_error():
"""Test that _generate_azure_ad_redis_token raises ImportError when azure-identity is missing."""
from unittest.mock import patch
from litellm._redis import _generate_azure_ad_redis_token

original_import = __builtins__["__import__"]

def mock_import(name, *args, **kwargs):
if name == "azure.identity":
raise ImportError("No module named 'azure.identity'")
return original_import(name, *args, **kwargs)

with patch("builtins.__import__", side_effect=mock_import):
with pytest.raises(ImportError) as exc_info:
_generate_azure_ad_redis_token()

assert "azure-identity is required" in str(exc_info.value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import-error test silently passes when azure-identity is already installed

The test patches builtins.__import__, but Python does not call __import__ for modules that are already present in sys.modules. If azure-identity is installed in the CI environment (or was loaded by an earlier test), azure.identity will already be cached in sys.modules, the mock is never invoked, no ImportError is raised, and pytest.raises(ImportError) fails the test — or, worse, the assertion silently passes because the exception was never raised and the pytest.raises block exited cleanly with a different path.

The reliable way to simulate a missing module is to set the key to None in sys.modules via patch.dict, which is how Python itself marks an import as explicitly unavailable:

def test_generate_azure_ad_redis_token_import_error():
    from litellm._redis import _generate_azure_ad_redis_token

    with patch.dict("sys.modules", {"azure.identity": None, "azure": None}):
        with pytest.raises(ImportError) as exc_info:
            _generate_azure_ad_redis_token()

    assert "azure-identity is required" in str(exc_info.value)

The same issue exists in test_generate_gcp_iam_access_token_import_error (line 2702), which patches builtins.__import__ in the same fragile way.

Comment thread litellm/_redis.py
Comment on lines +636 to +642
try:
access_token = _generate_azure_ad_redis_token(
azure_client_id=_az_client_id,
azure_tenant_id=_az_tenant_id,
azure_client_secret=_az_client_secret,
)
cluster_kwargs["password"] = access_token

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Azure AD token generated once for async cluster — will expire

For async Redis cluster, the Azure AD token is fetched a single time at client-creation and stored as a static password. Azure AD tokens typically have a short TTL (around 1 hour). Once the token expires, every cluster operation will raise an authentication error until the entire RedisCluster object is reconstructed. There is no automatic re-fetch path here.

This is a meaningful divergence from the sync path, where redis_connect_func is invoked on every new connection and therefore obtains a fresh token automatically on reconnects.

Note that the GCP async-cluster path (line 615) has the exact same limitation. A minimal mitigation would be to document this constraint prominently and/or integrate a periodic token-refresh mechanism (e.g., a background task that recreates the cluster client before expiry) so long-running deployments don't silently break.

Comment thread litellm/_redis.py Outdated
Comment on lines +396 to +413
_azure_redis_ad_token = redis_kwargs.get("azure_redis_ad_token") or get_secret(
"REDIS_AZURE_AD_TOKEN"
)

if (
_azure_redis_ad_token is not None
and str(_azure_redis_ad_token).lower() == "true"
and _gcp_service_account is not None
):
verbose_logger.warning(
"Both GCP IAM (gcp_service_account) and Azure AD (azure_redis_ad_token) are configured for Redis. "
"Using GCP IAM. Remove one to avoid misconfiguration."
)
# Clean up Azure-specific kwargs even though we're not using Azure AD
redis_kwargs.pop("azure_redis_ad_token", None)
redis_kwargs.pop("azure_client_id", None)
redis_kwargs.pop("azure_tenant_id", None)
redis_kwargs.pop("azure_client_secret", None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Azure custom kwargs leak into redis.Redis() when azure_redis_ad_token is not "true"

The cleanup of azure_redis_ad_token, azure_client_id, azure_tenant_id, and azure_client_secret from redis_kwargs only happens inside the two branches that are gated on str(_azure_redis_ad_token).lower() == "true". If someone sets REDIS_AZURE_AD_TOKEN to any value other than "true" (e.g., "false"), _azure_redis_ad_token is non-None, neither branch executes, and all four Azure-specific keys remain in redis_kwargs. They then get forwarded to redis.Redis(**redis_kwargs), which raises a TypeError because Redis doesn't accept these custom parameters.

A fix would be to always remove the Azure-specific keys before returning from _get_redis_client_logic, regardless of which branch is taken — similar to popping the GCP keys at the end of the GCP block.

Comment thread litellm/_redis.py
Comment on lines +292 to 303
self.send_command("AUTH", access_token, check_health=False)
auth_response = self.read_response()

if str_if_bytes(auth_response) != "OK":
raise AuthenticationError("Azure AD authentication failed for Redis")

return ad_connect


def get_redis_url_from_environment():
if "REDIS_URL" in os.environ:
return os.environ["REDIS_URL"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent empty-username handling between sync and async paths

In the sync ad_connect closure, username defaults to "" when REDIS_USERNAME is not set, and AUTH "" <token> is always sent. In the async cluster path (around line 512–514), the username is only set if non-empty:

# async path (cluster)
_username = os.environ.get("REDIS_USERNAME", "")
if _username:
    cluster_kwargs["username"] = _username

Azure Cache for Redis with AAD authentication requires the username to be the principal's Object ID. Sending an empty string (AUTH "" <token>) is not valid for most ACL-configured Azure Redis instances and will result in a hard AuthenticationError that the AuthenticationWrongNumberOfArgsError fallback will not catch (since the argument count is correct; it's the value that's wrong).

The sync path should mirror the async path and only include the username when it's non-empty:

username = os.environ.get("REDIS_USERNAME", "")
if username:
    auth_args = (username, access_token)
else:
    auth_args = (access_token,)
self.send_command("AUTH", *auth_args, check_health=False)

Comment thread litellm/_redis.py
Comment on lines +228 to +259
_client_secret = azure_client_secret or os.environ.get("AZURE_CLIENT_SECRET")

if _client_id and _tenant_id and _client_secret:
credential = ClientSecretCredential(
client_id=_client_id,
tenant_id=_tenant_id,
client_secret=_client_secret,
)
elif _client_id:
credential = ManagedIdentityCredential(client_id=_client_id)
else:
credential = DefaultAzureCredential()

token = credential.get_token(AZURE_REDIS_SCOPE)
return token.token


def create_azure_ad_redis_connect_func(
azure_client_id: Optional[str] = None,
azure_tenant_id: Optional[str] = None,
azure_client_secret: Optional[str] = None,
) -> Callable:
"""
Creates a custom Redis connection function for Azure AD authentication.

Used for sync Redis clients. Generates a fresh Azure AD token on each
connection/reconnection, ensuring token refresh is handled automatically.

Args:
azure_client_id: Optional Azure client ID
azure_tenant_id: Optional Azure tenant ID
azure_client_secret: Optional Azure client secret

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New credential object created on every Redis connection

_generate_azure_ad_redis_token instantiates a brand-new ClientSecretCredential, ManagedIdentityCredential, or DefaultAzureCredential on every call. Because this function is invoked inside ad_connect (which runs on every Redis connection and reconnection in the sync path), a fresh credential object and its internal HTTP client/session pool is created repeatedly throughout the application's lifetime.

Azure SDK credentials are designed to be long-lived — they cache tokens internally and handle expiry/refresh transparently. Recreating them per-connection means:

  • Token caching is bypassed (a new token request is sent to Azure AD on each reconnect)
  • Each ClientSecretCredential / ManagedIdentityCredential allocates its own HTTP connection pool that is never released, leading to resource exhaustion under load

The credential should be created once inside create_azure_ad_redis_connect_func (captured by the closure) and reused across connections. This is analogous to how service_account is captured in the outer scope of create_gcp_iam_redis_connect_func. The _generate_azure_ad_redis_token helper can still be called with the pre-built credential, or get_token can be called directly on it inside ad_connect — either way the SDK handles caching and silent renewal of the underlying token.

Comment thread litellm/_redis.py
Comment on lines +648 to +666
redis_connect_func, "_azure_redis_ad_token"
):
_az_client_id = getattr(redis_connect_func, "_azure_client_id", None)
_az_tenant_id = getattr(redis_connect_func, "_azure_tenant_id", None)
_az_client_secret = getattr(
redis_connect_func, "_azure_client_secret", None
)

verbose_logger.debug("Generating Azure AD token for async Redis cluster")
try:
access_token = _generate_azure_ad_redis_token(
azure_client_id=_az_client_id,
azure_tenant_id=_az_tenant_id,
azure_client_secret=_az_client_secret,
)
cluster_kwargs["password"] = access_token
# Set username if available
_username = os.environ.get("REDIS_USERNAME", "")
if _username:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Token expiry not documented for connection pool path

Like the async cluster path (which has an explicit NOTE warning about token TTL), the connection pool path fetches a one-shot Azure AD token at pool-creation time and sets it as a static password. Azure AD tokens typically expire in ~1 hour. Once the token expires, every connection checkout from the pool will fail authentication, and the pool itself has no refresh mechanism.

This is the same fundamental limitation called out in the cluster block, but there is no equivalent warning here. Consider adding a comment to make the constraint visible to future maintainers:

# Handle Azure AD / GCP IAM auth for connection pool — async pools don't
# support redis_connect_func, so resolve the token now and set as password.
# NOTE: The token is fetched once at pool creation. Azure AD tokens typically
# expire in ~1 hour; once expired, all connections from this pool will fail
# with an auth error. The pool (and the LiteLLM Redis client that owns it)
# must be recreated to obtain a fresh token.
redis_connect_func = redis_kwargs.pop("redis_connect_func", None)

Comment thread litellm/_redis.py
Comment on lines +453 to +455
redis_kwargs["redis_connect_func"]._azure_tenant_id = _azure_tenant_id
redis_kwargs["redis_connect_func"]._azure_client_secret = _azure_client_secret

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Azure credential value stored as plaintext attribute on function object

The _azure_client_secret value is stored as a plain attribute on the redis_connect_func closure object. Unlike the GCP path, which only attaches the service account name (not a credential value), this exposes a raw auth value that could be inadvertently captured in debug logs, stack traces, or any introspection of redis_kwargs (e.g. repr(redis_connect_func)).

The credential object built inside _build_azure_credential already encapsulates this value internally and handles all token fetching. A cleaner approach for the async cluster path would be to attach the pre-built credential object to redis_connect_func rather than the raw credential components, so the raw value never needs to be re-extracted outside the closure.

Comment on lines +2800 to +2818
def test_redis_client_logic_azure_ad_auth():
"""Test that _get_redis_client_logic sets up Azure AD auth when REDIS_AZURE_AD_TOKEN=true."""
from litellm._redis import _get_redis_client_logic

redis_kwargs = _get_redis_client_logic(
host="myredis.redis.cache.windows.net",
port="6380",
azure_redis_ad_token="true",
ssl=True,
)

# Should have redis_connect_func set
assert "redis_connect_func" in redis_kwargs
assert hasattr(redis_kwargs["redis_connect_func"], "_azure_redis_ad_token")
assert redis_kwargs["redis_connect_func"]._azure_redis_ad_token is True

# Azure-specific kwargs should be removed
assert "azure_redis_ad_token" not in redis_kwargs
assert "azure_client_id" not in redis_kwargs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test will fail in CI without azure-identity installed

This test calls _get_redis_client_logic with the Azure AD flag enabled, which immediately invokes create_azure_ad_redis_connect_func_build_azure_credentialfrom azure.identity import .... Because the import is eager (not deferred inside the connect closure), an ImportError is raised at test setup time if the optional azure-identity package is not present.

All other Azure tests in this PR correctly use patch.dict("sys.modules", ...) for isolation, but this test does not. With no credentials provided, DefaultAzureCredential() is instantiated during _get_redis_client_logic, requiring the actual package to be installed.

The fix mirrors the pattern used by test_generate_azure_ad_redis_token: mock azure.identity via patch.dict before calling _get_redis_client_logic, so the test is self-contained and does not depend on the optional package being present in CI.

@shivamrawat1

shivamrawat1 commented May 9, 2026

Copy link
Copy Markdown
Collaborator

closing in favour of #27556

shivamrawat1 added a commit that referenced this pull request May 9, 2026
Address review issues on PR #21764 cherry-pick:

- Add AzureADCredentialProvider that wraps the live azure-identity
  credential, so async cluster, async standard, and connection-pool
  paths fetch tokens via the SDK's internal cache + silent refresh on
  every connection — instead of baking a single point-in-time token as
  the password (which would expire ~1h after pool creation and break
  all subsequent reconnects).
- Stop attaching raw client_id / tenant_id / client_secret to the
  redis_connect_func object. The credential closure already holds them;
  exposing them as function attributes risked leaks via inspection or
  logging. Async paths now read the already-built credential object via
  `_azure_credential` instead.
- Connection-pool path now picks up REDIS_USERNAME for ACL-configured
  Azure Redis instances, matching the cluster + async paths.
- Mock azure.identity via sys.modules in test_redis_client_logic_
  azure_ad_auth so the test no longer requires azure-identity to be
  installed in the CI environment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants