Skip to content

Commit 1d52a04

Browse files
[Core] Fix MSAL to respect REQUESTS_CA_BUNDLE for proxy certificates
Problem: - When using 'az login' behind a proxy with self-signed certificates, MSAL authentication fails with SSL certificate verification errors - While other Azure CLI operations properly respect the REQUESTS_CA_BUNDLE environment variable, MSAL library doesn't use the custom CA bundle - This causes authentication to fail even when users correctly configure their proxy certificates Solution: - Added get_msal_http_client() helper function in _debug.py that creates a configured requests.Session - Updated Identity._msal_app_kwargs to include http_client parameter with proper certificate configuration - Modified ManagedIdentityCredential to use the configured HTTP client instead of creating its own - The fix ensures all MSAL-based authentication respects certificate settings consistently Testing: - Added unit test test_get_msal_http_client_respects_ca_bundle() to verify proper configuration - All existing tests pass without regression - Code style and linting checks pass This change allows users working behind corporate proxies to successfully authenticate with Azure CLI by properly configuring their custom certificate bundles via the REQUESTS_CA_BUNDLE environment variable, matching the behavior of other Azure SDK operations.
1 parent 8d0c503 commit 1d52a04

File tree

4 files changed

+84
-3
lines changed

4 files changed

+84
-3
lines changed

src/azure-cli-core/azure/cli/core/_debug.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,32 @@ def change_ssl_cert_verification_track2():
4545
logger.debug("Using CA bundle file at '%s'.", ca_bundle_file)
4646
client_kwargs['connection_verify'] = ca_bundle_file
4747
return client_kwargs
48+
49+
50+
def get_msal_http_client():
51+
"""
52+
Create an HTTP client (requests.Session) for MSAL that respects certificate verification settings.
53+
54+
This ensures MSAL applications use the same certificate verification settings as the rest of Azure CLI,
55+
including custom CA bundles specified via REQUESTS_CA_BUNDLE environment variable.
56+
57+
Returns:
58+
requests.Session: A configured Session object with appropriate certificate verification settings.
59+
"""
60+
import requests
61+
62+
session = requests.Session()
63+
64+
if should_disable_connection_verify():
65+
logger.warning("Connection verification disabled by environment variable %s",
66+
DISABLE_VERIFY_VARIABLE_NAME)
67+
os.environ[ADAL_PYTHON_SSL_NO_VERIFY] = '1'
68+
session.verify = False
69+
elif REQUESTS_CA_BUNDLE in os.environ:
70+
ca_bundle_file = os.environ[REQUESTS_CA_BUNDLE]
71+
if not os.path.isfile(ca_bundle_file):
72+
raise CLIError('REQUESTS_CA_BUNDLE environment variable is specified with an invalid file path')
73+
logger.debug("MSAL: Using CA bundle file at '%s'.", ca_bundle_file)
74+
session.verify = ca_bundle_file
75+
76+
return session

src/azure-cli-core/azure/cli/core/auth/identity.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,14 @@ def _msal_app_kwargs(self):
9999
if self._use_msal_http_cache and not Identity._msal_http_cache:
100100
Identity._msal_http_cache = self._load_msal_http_cache()
101101

102+
# Import here to avoid circular dependency
103+
from azure.cli.core._debug import get_msal_http_client
104+
102105
return {
103106
"authority": self._msal_authority,
104107
"token_cache": Identity._msal_token_cache,
105108
"http_cache": Identity._msal_http_cache,
109+
"http_client": get_msal_http_client(),
106110
"instance_discovery": self._instance_discovery,
107111
# CP1 means we can handle claims challenges (CAE)
108112
"client_capabilities": None if "AZURE_IDENTITY_DISABLE_CP1" in os.environ else ["CP1"]

src/azure-cli-core/azure/cli/core/auth/msal_credentials.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,14 @@ class ManagedIdentityCredential: # pylint: disable=too-few-public-methods
139139
"""
140140

141141
def __init__(self, client_id=None, resource_id=None, object_id=None):
142-
import requests
142+
# Use the configured HTTP client that respects certificate settings
143+
from azure.cli.core._debug import get_msal_http_client
143144
if client_id or resource_id or object_id:
144145
managed_identity = UserAssignedManagedIdentity(
145146
client_id=client_id, resource_id=resource_id, object_id=object_id)
146147
else:
147148
managed_identity = SystemAssignedManagedIdentity()
148-
self._msal_client = ManagedIdentityClient(managed_identity, http_client=requests.Session())
149+
self._msal_client = ManagedIdentityClient(managed_identity, http_client=get_msal_http_client())
149150

150151
def acquire_token(self, scopes, **kwargs):
151152
logger.debug("ManagedIdentityCredential.acquire_token: scopes=%r, kwargs=%r", scopes, kwargs)

src/azure-cli-core/azure/cli/core/tests/test_connection_verify.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
import os
77
import logging
88
import unittest
9-
from unittest.mock import MagicMock
9+
from unittest.mock import MagicMock, patch
10+
import tempfile
1011

1112
import azure.cli.core._debug as _debug
1213
import azure.cli.core.util as cli_util
@@ -35,6 +36,52 @@ def test_verify_client_connection(self):
3536
clientMock = _debug.change_ssl_cert_verification(clientMock)
3637
self.assertFalse(clientMock.config.connection.verify)
3738

39+
def test_get_msal_http_client_respects_ca_bundle(self):
40+
"""Test that get_msal_http_client() respects REQUESTS_CA_BUNDLE environment variable."""
41+
# Save original environment
42+
original_ca_bundle = os.environ.get(_debug.REQUESTS_CA_BUNDLE)
43+
original_disable_verify = os.environ.get(cli_util.DISABLE_VERIFY_VARIABLE_NAME)
44+
45+
try:
46+
# Create a temporary file to act as a CA bundle
47+
with tempfile.NamedTemporaryFile(delete=False, suffix='.pem') as tmp_file:
48+
tmp_file.write(b'# Test CA Bundle')
49+
tmp_file_path = tmp_file.name
50+
51+
# Test 1: With REQUESTS_CA_BUNDLE set
52+
os.environ[_debug.REQUESTS_CA_BUNDLE] = tmp_file_path
53+
if cli_util.DISABLE_VERIFY_VARIABLE_NAME in os.environ:
54+
del os.environ[cli_util.DISABLE_VERIFY_VARIABLE_NAME]
55+
56+
session = _debug.get_msal_http_client()
57+
self.assertEqual(session.verify, tmp_file_path)
58+
59+
# Test 2: With connection verification disabled
60+
del os.environ[_debug.REQUESTS_CA_BUNDLE]
61+
os.environ[cli_util.DISABLE_VERIFY_VARIABLE_NAME] = "1"
62+
63+
session = _debug.get_msal_http_client()
64+
self.assertFalse(session.verify)
65+
66+
# Test 3: With neither set (default behavior)
67+
del os.environ[cli_util.DISABLE_VERIFY_VARIABLE_NAME]
68+
69+
session = _debug.get_msal_http_client()
70+
self.assertTrue(session.verify) # Default is True
71+
72+
finally:
73+
# Cleanup
74+
os.unlink(tmp_file_path)
75+
# Restore original environment
76+
if original_ca_bundle:
77+
os.environ[_debug.REQUESTS_CA_BUNDLE] = original_ca_bundle
78+
elif _debug.REQUESTS_CA_BUNDLE in os.environ:
79+
del os.environ[_debug.REQUESTS_CA_BUNDLE]
80+
if original_disable_verify:
81+
os.environ[cli_util.DISABLE_VERIFY_VARIABLE_NAME] = original_disable_verify
82+
elif cli_util.DISABLE_VERIFY_VARIABLE_NAME in os.environ:
83+
del os.environ[cli_util.DISABLE_VERIFY_VARIABLE_NAME]
84+
3885

3986
if __name__ == '__main__':
4087
unittest.main()

0 commit comments

Comments
 (0)