Skip to content

Commit 1126061

Browse files
authored
[Identity] Prepare beta release (#45135)
Signed-off-by: Paul Van Eck <paulvaneck@microsoft.com>
1 parent 4e7d80b commit 1126061

13 files changed

Lines changed: 1774 additions & 3 deletions

sdk/identity/azure-identity/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Release History
22

3+
## 1.26.0b2 (2026-02-11)
4+
5+
### Breaking Changes
6+
7+
> These changes do not impact the API of stable versions such as 1.25.2.
8+
> Only code written against beta version 1.26.0b1 is affected.
9+
- Renamed `use_token_proxy` keyword argument to `enable_azure_proxy` in `WorkloadIdentityCredential` to better reflect its purpose. ([#44147](https://github.com/Azure/azure-sdk-for-python/pull/44147))
10+
311
## 1.25.2 (2026-02-10)
412

513
### Bugs Fixed

sdk/identity/azure-identity/TROUBLESHOOTING.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,12 @@ Get-AzAccessToken -ResourceUrl "https://management.core.windows.net"
278278
|---|---|---|
279279
|WorkloadIdentityCredential authentication unavailable. The workload options are not fully configured|The `WorkloadIdentityCredential` requires `client_id`, `tenant_id` and `token_file_path` to authenticate with Microsoft Entra ID.| <ul><li>If using `DefaultAzureCredential` then:</li><ul><li>Ensure client ID is specified via the `workload_identity_client_id` keyword argument or the `AZURE_CLIENT_ID` env variable.</li><li>Ensure tenant ID is specified via the `AZURE_TENANT_ID` env variable.</li><li>Ensure token file path is specified via `AZURE_FEDERATED_TOKEN_FILE` env variable.</li><li>Ensure authority host is specified via `AZURE_AUTHORITY_HOST` env variable.</ul><li>If using `WorkloadIdentityCredential` then:</li><ul><li>Ensure tenant ID is specified via the `tenant_id` keyword argument or the `AZURE_TENANT_ID` env variable.</li><li>Ensure client ID is specified via the `client_id` keyword argument or the `AZURE_CLIENT_ID` env variable.</li><li>Ensure token file path is specified via the `token_file_path` keyword argument or the `AZURE_FEDERATED_TOKEN_FILE` environment variable. </li></ul></li><li>Consult the [product troubleshooting guide](https://azure.github.io/azure-workload-identity/docs/troubleshooting.html) for other issues.</li></ul>|
280280
281+
#### `ClientAuthenticationError` for applications using [Azure Kubernetes Service identity bindings](https://learn.microsoft.com/azure/aks/identity-bindings-concepts)
282+
283+
| Error Message |Description| Mitigation |
284+
|---|---|---|
285+
|<ul><li>AADSTS700211: No matching federated identity record found for presented assertion issuer ...</li><li>AADSTS700212: No matching federated identity record found for presented assertion audience 'api://AKSIdentityBinding'.</li></ul> |`WorkloadIdentityCredential` isn't configured to use the identity binding proxy|Set the `enable_azure_proxy` keyword argument to `True` when creating `WorkloadIdentityCredential`. Note that identity binding mode isn't supported when `WorkloadIdentityCredential` is used via `DefaultAzureCredential`. `WorkloadIdentityCredential` should be used directly in this scenario.|
286+
281287
## Troubleshoot `AzurePipelinesCredential` authentication issues
282288
283289
[comment]: # ( cspell:ignore oidcrequesturi )

sdk/identity/azure-identity/azure/identity/_constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ class EnvironmentVariables:
6868
AZURE_REGIONAL_AUTHORITY_NAME = "AZURE_REGIONAL_AUTHORITY_NAME"
6969

7070
AZURE_FEDERATED_TOKEN_FILE = "AZURE_FEDERATED_TOKEN_FILE"
71+
AZURE_KUBERNETES_SNI_NAME = "AZURE_KUBERNETES_SNI_NAME"
72+
AZURE_KUBERNETES_TOKEN_PROXY = "AZURE_KUBERNETES_TOKEN_PROXY"
73+
AZURE_KUBERNETES_CA_FILE = "AZURE_KUBERNETES_CA_FILE"
74+
AZURE_KUBERNETES_CA_DATA = "AZURE_KUBERNETES_CA_DATA"
7175

7276
AZURE_TOKEN_CREDENTIALS = "AZURE_TOKEN_CREDENTIALS"
7377
WORKLOAD_IDENTITY_VARS = (AZURE_AUTHORITY_HOST, AZURE_TENANT_ID, AZURE_FEDERATED_TOKEN_FILE)

sdk/identity/azure-identity/azure/identity/_credentials/workload_identity.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,18 @@
99

1010
from .client_assertion import ClientAssertionCredential
1111
from .._constants import EnvironmentVariables
12+
from .._internal import within_credential_chain
1213

1314

1415
WORKLOAD_CONFIG_ERROR = (
1516
"WorkloadIdentityCredential authentication unavailable. The workload options are not fully "
1617
"configured. See the troubleshooting guide for more information: "
1718
"https://aka.ms/azsdk/python/identity/workloadidentitycredential/troubleshoot"
1819
)
20+
CA_DATA_FILE_ERROR = "Both AZURE_KUBERNETES_CA_FILE and AZURE_KUBERNETES_CA_DATA are set. Only one should be set."
21+
CUSTOM_PROXY_ENV_ERROR = (
22+
"AZURE_KUBERNETES_TOKEN_PROXY is not set but other custom endpoint-related environment variables are present."
23+
)
1924

2025

2126
class TokenFileMixin:
@@ -100,10 +105,51 @@ def __init__(
100105

101106
self._token_file_path = token_file_path
102107

108+
if kwargs.pop("enable_azure_proxy", False) and not within_credential_chain.get():
109+
token_proxy_endpoint = os.environ.get(EnvironmentVariables.AZURE_KUBERNETES_TOKEN_PROXY)
110+
sni = os.environ.get(EnvironmentVariables.AZURE_KUBERNETES_SNI_NAME)
111+
ca_file = os.environ.get(EnvironmentVariables.AZURE_KUBERNETES_CA_FILE)
112+
ca_data = os.environ.get(EnvironmentVariables.AZURE_KUBERNETES_CA_DATA)
113+
if token_proxy_endpoint:
114+
if ca_file and ca_data:
115+
raise ValueError(CA_DATA_FILE_ERROR)
116+
117+
transport = _get_transport(
118+
sni=sni,
119+
token_proxy_endpoint=token_proxy_endpoint,
120+
ca_file=ca_file,
121+
ca_data=ca_data,
122+
)
123+
124+
if transport:
125+
kwargs["transport"] = transport
126+
else:
127+
raise ValueError(
128+
"Transport creation failed. Ensure that the requests package is installed to enable token "
129+
"proxy usage in this credential."
130+
)
131+
elif sni or ca_file or ca_data:
132+
raise ValueError(CUSTOM_PROXY_ENV_ERROR)
133+
103134
super(WorkloadIdentityCredential, self).__init__(
104135
tenant_id=tenant_id,
105136
client_id=client_id,
106137
func=self._get_service_account_token,
107138
token_file_path=token_file_path,
108139
**kwargs,
109140
)
141+
142+
143+
def _get_transport(sni, token_proxy_endpoint, ca_file, ca_data):
144+
try:
145+
from .._internal.token_binding_transport_requests import CustomRequestsTransport
146+
147+
return CustomRequestsTransport(
148+
sni=sni,
149+
proxy_endpoint=token_proxy_endpoint,
150+
ca_file=ca_file,
151+
ca_data=ca_data,
152+
)
153+
154+
except ImportError:
155+
return None
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# ------------------------------------
2+
# Copyright (c) Microsoft Corporation.
3+
# Licensed under the MIT License.
4+
# ------------------------------------
5+
# cspell:ignore cafile
6+
import os
7+
import urllib.parse
8+
from typing import Optional, Any
9+
10+
from azure.core.rest import HttpRequest
11+
12+
13+
class TokenBindingTransportMixin:
14+
"""Mixin class providing URL validation, CA file tracking, and proxy URL functionality for transport classes."""
15+
16+
def __init__(self, **kwargs: Any) -> None:
17+
"""Initialize CA file tracking and proxy attributes."""
18+
self._ca_file = kwargs.pop("ca_file", None)
19+
self._ca_data = kwargs.pop("ca_data", None)
20+
self._proxy_endpoint = kwargs.pop("proxy_endpoint", None)
21+
self._sni = kwargs.pop("sni", None)
22+
23+
self._ca_file_mtime: Optional[float] = None
24+
25+
if self._ca_file and self._ca_data:
26+
raise ValueError("Both ca_file and ca_data are set. Only one should be set")
27+
28+
if self._proxy_endpoint:
29+
self._validate_url(self._proxy_endpoint)
30+
31+
# If we have a ca_file, read it once and store as ca_data
32+
if self._ca_file:
33+
self._load_ca_file_to_data()
34+
35+
super().__init__()
36+
37+
def _validate_url(self, url: str) -> None:
38+
"""Validate that a URL meets security requirements for HTTPS connections.
39+
40+
:param url: The URL to validate.
41+
:type url: str
42+
:raises ValueError: If the URL does not meet security requirements.
43+
"""
44+
parsed_url = urllib.parse.urlparse(url)
45+
if parsed_url.scheme != "https":
46+
raise ValueError(f"Endpoint URL ({url}) must use the 'https' scheme. Got '{parsed_url.scheme}' instead.")
47+
if parsed_url.username or parsed_url.password:
48+
raise ValueError(f"Endpoint URL ({url}) must not contain username or password.")
49+
if parsed_url.fragment:
50+
raise ValueError(f"Endpoint URL ({url}) must not contain a fragment.")
51+
if parsed_url.query:
52+
raise ValueError(f"Endpoint URL ({url}) must not contain query parameters.")
53+
54+
def _load_ca_file_to_data(self) -> None:
55+
"""Load CA file content into ca_data and track modification time.
56+
57+
:raises ValueError: If the CA file is empty on first read.
58+
"""
59+
try:
60+
with open(self._ca_file, "r", encoding="utf-8") as f:
61+
content = f.read()
62+
63+
# Check if the file is empty
64+
if not content:
65+
# If no prior ca_data exists (first read), fail
66+
if self._ca_data is None:
67+
raise ValueError(f"CA file ({self._ca_file}) is empty. Cannot establish secure connection.")
68+
# If we had prior ca_data, keep it (mid-rotation scenario)
69+
return
70+
71+
# File has content, update ca_data and tracking
72+
self._ca_data = content
73+
self._ca_file_mtime = os.path.getmtime(self._ca_file)
74+
except (OSError, IOError) as e:
75+
# If no prior ca_data exists (first read), fail
76+
if self._ca_data is None:
77+
raise ValueError(f"Failed to read CA file ({self._ca_file}): {e}") from e
78+
# If we can't read the file, keep existing ca_data but clear mtime
79+
# so we'll try to reload on the next change check
80+
self._ca_file_mtime = None
81+
82+
def _has_ca_file_changed(self) -> bool:
83+
"""Check if the CA file has changed since last load.
84+
85+
:return: True if the CA file has changed, False otherwise.
86+
:rtype: bool
87+
"""
88+
if not self._ca_file:
89+
return False
90+
91+
if not os.path.exists(self._ca_file):
92+
# File was deleted, consider this a change if we had data before
93+
return self._ca_data is not None or self._ca_file_mtime is not None
94+
95+
try:
96+
# Check modification time
97+
current_mtime = os.path.getmtime(self._ca_file)
98+
return self._ca_file_mtime != current_mtime
99+
except (OSError, IOError):
100+
# If we can't read the file stats, assume it changed
101+
return True
102+
103+
def _update_request_url(self, request: HttpRequest) -> None:
104+
"""Update the request URL to use proxy endpoint if configured.
105+
106+
:param request: The HTTP request object to update.
107+
:type request: ~azure.core.rest.HttpRequest
108+
"""
109+
if self._proxy_endpoint:
110+
parsed_request_url = urllib.parse.urlparse(request.url)
111+
parsed_proxy_url = urllib.parse.urlparse(self._proxy_endpoint)
112+
combined_path = parsed_proxy_url.path.rstrip("/") + "/" + parsed_request_url.path.lstrip("/")
113+
new_url = urllib.parse.urlunparse(
114+
(
115+
parsed_proxy_url.scheme,
116+
parsed_proxy_url.netloc,
117+
combined_path,
118+
parsed_request_url.params,
119+
parsed_request_url.query,
120+
parsed_request_url.fragment,
121+
)
122+
)
123+
request.url = new_url
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# ------------------------------------
2+
# Copyright (c) Microsoft Corporation.
3+
# Licensed under the MIT License.
4+
# ------------------------------------
5+
"""
6+
Requests transport class for WorkloadIdentityCredential with token proxy support.
7+
"""
8+
import ssl
9+
from typing import Any, Optional
10+
11+
from requests.adapters import HTTPAdapter
12+
from requests import Session
13+
from azure.core.pipeline.transport import ( # pylint: disable=non-abstract-transport-import, no-name-in-module
14+
RequestsTransport,
15+
)
16+
from azure.core.rest import HttpRequest
17+
18+
from .token_binding_transport_mixin import TokenBindingTransportMixin
19+
20+
21+
class SNIAdapter(HTTPAdapter):
22+
"""A custom HTTPAdapter that allows setting a custom SNI hostname."""
23+
24+
def __init__(self, server_hostname: Optional[str], ca_data: Optional[str], **kwargs: Any) -> None:
25+
self.server_hostname = server_hostname
26+
self.ca_data = ca_data
27+
super().__init__(**kwargs)
28+
29+
def init_poolmanager(self, connections: int, maxsize: int, block: bool = False, **pool_kwargs: Any) -> None:
30+
if self.server_hostname:
31+
pool_kwargs["server_hostname"] = self.server_hostname
32+
pool_kwargs["ssl_context"] = ssl.create_default_context(cadata=self.ca_data)
33+
super().init_poolmanager(connections, maxsize, block, **pool_kwargs)
34+
35+
36+
class CustomRequestsTransport(TokenBindingTransportMixin, RequestsTransport):
37+
"""Custom RequestsTransport with SNI and CA certificate support for WorkloadIdentityCredential."""
38+
39+
def __init__(self, *args: Any, **kwargs: Any) -> None:
40+
self.session: Optional[Session] = None
41+
super().__init__(*args, **kwargs)
42+
self._update_adaptor()
43+
44+
def _update_adaptor(self) -> None:
45+
"""Update the session's adapter with the current SNI and CA data."""
46+
if not self.session:
47+
self.session = Session()
48+
49+
adapter = SNIAdapter(self._sni, self._ca_data)
50+
self.session.mount("https://", adapter)
51+
52+
def send(self, request: HttpRequest, **kwargs: Any) -> Any:
53+
self._update_request_url(request)
54+
55+
# Check if CA file has changed and reload ca_data if needed
56+
if self._ca_file and self._has_ca_file_changed():
57+
self._load_ca_file_to_data()
58+
# If ca_data was updated, recreate SSL context with the new data
59+
if self._ca_data:
60+
self._update_adaptor()
61+
return super().send(request, **kwargs)

sdk/identity/azure-identity/azure/identity/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
# Copyright (c) Microsoft Corporation.
33
# Licensed under the MIT License.
44
# ------------------------------------
5-
VERSION = "1.25.2"
5+
VERSION = "1.26.0b2"

sdk/identity/azure-identity/azure/identity/aio/_credentials/workload_identity.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@
77
from typing import Any, Optional
88

99
from .client_assertion import ClientAssertionCredential
10-
from ..._credentials.workload_identity import TokenFileMixin, WORKLOAD_CONFIG_ERROR
10+
from ..._credentials.workload_identity import (
11+
TokenFileMixin,
12+
WORKLOAD_CONFIG_ERROR,
13+
CA_DATA_FILE_ERROR,
14+
CUSTOM_PROXY_ENV_ERROR,
15+
)
1116
from ..._constants import EnvironmentVariables
17+
from ..._internal import within_credential_chain
1218

1319

1420
class WorkloadIdentityCredential(ClientAssertionCredential, TokenFileMixin):
@@ -75,10 +81,61 @@ def __init__(
7581

7682
self._token_file_path = token_file_path
7783

84+
if kwargs.pop("enable_azure_proxy", False) and not within_credential_chain.get():
85+
token_proxy_endpoint = os.environ.get(EnvironmentVariables.AZURE_KUBERNETES_TOKEN_PROXY)
86+
sni = os.environ.get(EnvironmentVariables.AZURE_KUBERNETES_SNI_NAME)
87+
ca_file = os.environ.get(EnvironmentVariables.AZURE_KUBERNETES_CA_FILE)
88+
ca_data = os.environ.get(EnvironmentVariables.AZURE_KUBERNETES_CA_DATA)
89+
if token_proxy_endpoint:
90+
if ca_file and ca_data:
91+
raise ValueError(CA_DATA_FILE_ERROR)
92+
93+
transport = _get_transport(
94+
sni=sni,
95+
token_proxy_endpoint=token_proxy_endpoint,
96+
ca_file=ca_file,
97+
ca_data=ca_data,
98+
)
99+
100+
if transport:
101+
kwargs["transport"] = transport
102+
else:
103+
raise ValueError(
104+
"Async transport creation failed. Ensure that the aiohttp or requests package is installed to "
105+
"enable token proxy usage in this credential."
106+
)
107+
elif sni or ca_file or ca_data:
108+
raise ValueError(CUSTOM_PROXY_ENV_ERROR)
109+
78110
super().__init__(
79111
tenant_id=tenant_id,
80112
client_id=client_id,
81113
func=self._get_service_account_token,
82114
token_file_path=token_file_path,
83115
**kwargs,
84116
)
117+
118+
119+
def _get_transport(sni, token_proxy_endpoint, ca_file, ca_data):
120+
try:
121+
from .._internal.token_binding_transport_aiohttp import CustomAioHttpTransport
122+
123+
return CustomAioHttpTransport(
124+
sni=sni,
125+
proxy_endpoint=token_proxy_endpoint,
126+
ca_file=ca_file,
127+
ca_data=ca_data,
128+
)
129+
except ImportError:
130+
# Fallback to async-wrapped requests transport
131+
try:
132+
from .._internal.token_binding_transport_asyncio import CustomAsyncioRequestsTransport
133+
134+
return CustomAsyncioRequestsTransport(
135+
sni=sni,
136+
proxy_endpoint=token_proxy_endpoint,
137+
ca_file=ca_file,
138+
ca_data=ca_data,
139+
)
140+
except ImportError:
141+
return None

0 commit comments

Comments
 (0)