Skip to content

Commit f6d7789

Browse files
authored
[Identity] Enable CAE toggle per token request (Azure#30777)
- All relevant credentials (User Credentials + Service Principal Credentials + SharedTokenCacheCredential) now accept and honor an enable_cae keyword argument. This denotes that the token request should include "CP1" client capabilities indicating that the SDK is ready to handle CAE claims challenges. - Two token caches are now maintained — one for non-CAE tokens and one for CAE-tokens. - The AZURE_IDENTITY_DISABLE_CP1 environment variable is removed since the behavior of the CP1 capability being "always-on" has been changed. Signed-off-by: Paul Van Eck <paulvaneck@microsoft.com>
1 parent 8fb77fc commit f6d7789

28 files changed

+696
-342
lines changed

sdk/identity/azure-identity/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@
44

55
### Features Added
66

7+
- Continuous Access Evaluation (CAE) is now configurable per-request by setting the `enable_cae` keyword argument to `True` in `get_token`. This applies to user credentials and service principal credentials. ([#30777](https://github.com/Azure/azure-sdk-for-python/pull/30777))
8+
79
### Breaking Changes
810

11+
- CP1 client capabilities for CAE is no longer always-on by default for user credentials. This capability will now be configured as-needed in each `get_token` request by each SDK. ([#30777](https://github.com/Azure/azure-sdk-for-python/pull/30777))
12+
- Suffixes are now appended to persistent cache names to indicate whether CAE or non-CAE tokens are stored in the cache. This is to prevent CAE and non-CAE tokens from being mixed/overwritten in the same cache. This could potentially cause issues if you are trying to share the same cache between applications that are using different versions of the Azure Identity library as each application would be reading from a different cache file.
13+
- Since CAE is no longer always enabled for user-credentials, the `AZURE_IDENTITY_DISABLE_CP1` environment variable is no longer supported.
14+
915
### Bugs Fixed
1016

1117
### Other Changes

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
DEFAULT_REFRESH_OFFSET = 300
1111
DEFAULT_TOKEN_REFRESH_RETRY_DELAY = 30
1212

13+
CACHE_NON_CAE_SUFFIX = ".nocae" # cspell:disable-line
14+
CACHE_CAE_SUFFIX = ".cae"
15+
1316

1417
class AzureAuthorityHosts:
1518
AZURE_CHINA = "login.chinacloudapi.cn"
@@ -50,5 +53,3 @@ class EnvironmentVariables:
5053

5154
AZURE_FEDERATED_TOKEN_FILE = "AZURE_FEDERATED_TOKEN_FILE"
5255
WORKLOAD_IDENTITY_VARS = (AZURE_AUTHORITY_HOST, AZURE_TENANT_ID, AZURE_FEDERATED_TOKEN_FILE)
53-
54-
AZURE_IDENTITY_DISABLE_CP1 = "AZURE_IDENTITY_DISABLE_CP1"

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

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ def get_token(self, *scopes: str, **kwargs: Any) -> AccessToken:
6464
https://learn.microsoft.com/azure/active-directory/develop/scopes-oidc.
6565
:keyword str claims: additional claims required in the token, such as those returned in a resource provider's
6666
claims challenge following an authorization failure
67-
67+
:keyword bool enable_cae: indicates whether to enable Continuous Access Evaluation (CAE) for the requested
68+
token. Defaults to False.
6869
:return: An access token with the desired scopes.
6970
:rtype: ~azure.core.credentials.AccessToken
7071
:raises ~azure.identity.CredentialUnavailableError: the cache is unavailable or contains insufficient user
@@ -100,20 +101,28 @@ def get_token(self, *scopes: str, **kwargs: Any) -> AccessToken:
100101
if not scopes:
101102
raise ValueError("'get_token' requires at least one scope")
102103

103-
if not self._initialized:
104-
self._initialize()
104+
if not self._client_initialized:
105+
self._initialize_client()
106+
107+
is_cae = bool(kwargs.get("enable_cae", False))
108+
token_cache = self._cae_cache if is_cae else self._cache
109+
110+
# Try to load the cache if it is None.
111+
if not token_cache:
112+
token_cache = self._initialize_cache(is_cae=is_cae)
105113

106-
if not self._cache:
107-
raise CredentialUnavailableError(message="Shared token cache unavailable")
114+
# If the cache is still None, raise an error.
115+
if not token_cache:
116+
raise CredentialUnavailableError(message="Shared token cache unavailable")
108117

109-
account = self._get_account(self._username, self._tenant_id)
118+
account = self._get_account(self._username, self._tenant_id, is_cae=is_cae)
110119

111-
token = self._get_cached_access_token(scopes, account)
120+
token = self._get_cached_access_token(scopes, account, is_cae=is_cae)
112121
if token:
113122
return token
114123

115124
# try each refresh token, returning the first access token acquired
116-
for refresh_token in self._get_refresh_tokens(account):
125+
for refresh_token in self._get_refresh_tokens(account, is_cae=is_cae):
117126
token = self._client.obtain_token_by_refresh_token(scopes, refresh_token, **kwargs)
118127
return token
119128

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

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22
# Copyright (c) Microsoft Corporation.
33
# Licensed under the MIT License.
44
# ------------------------------------
5-
import os
65
import platform
76
import time
87
from typing import Dict, Optional, Any
98

10-
from msal import PublicClientApplication
9+
from msal import PublicClientApplication, TokenCache
1110

1211
from azure.core.credentials import AccessToken
1312
from azure.core.exceptions import ClientAuthenticationError
@@ -18,7 +17,6 @@
1817
from .._internal.msal_client import MsalClient
1918
from .._internal.shared_token_cache import NO_TOKEN
2019
from .._persistent_cache import _load_persistent_cache, TokenCachePersistenceOptions
21-
from .._constants import EnvironmentVariables
2220
from .. import AuthenticationRecord
2321

2422

@@ -39,11 +37,15 @@ def __init__(
3937
self._tenant_id = tenant_id or self._auth_record.tenant_id
4038
validate_tenant_id(self._tenant_id)
4139
self._cache = kwargs.pop("_cache", None)
40+
self._cae_cache = kwargs.pop("_cae_cache", None)
41+
4242
self._cache_persistence_options = kwargs.pop("cache_persistence_options", None)
43+
4344
self._client_applications: Dict[str, PublicClientApplication] = {}
45+
self._cae_client_applications: Dict[str, PublicClientApplication] = {}
46+
4447
self._additionally_allowed_tenants = kwargs.pop("additionally_allowed_tenants", [])
4548
self._client = MsalClient(**kwargs)
46-
self._initialized = False
4749

4850
def __enter__(self):
4951
self._client.__enter__()
@@ -56,47 +58,69 @@ def get_token(self, *scopes: str, **kwargs: Any) -> AccessToken:
5658
if not scopes:
5759
raise ValueError('"get_token" requires at least one scope')
5860

59-
if not self._initialized:
60-
self._initialize()
61+
token_cache = self._cae_cache if kwargs.get("enable_cae") else self._cache
62+
63+
# Try to load the cache if it is None.
64+
if not token_cache:
65+
token_cache = self._initialize_cache(is_cae=bool(kwargs.get("enable_cae")))
6166

62-
if not self._cache:
63-
if within_dac.get():
64-
raise CredentialUnavailableError(message="Shared token cache unavailable")
65-
raise ClientAuthenticationError(message="Shared token cache unavailable")
67+
# If the cache is still None, raise an error.
68+
if not token_cache:
69+
if within_dac.get():
70+
raise CredentialUnavailableError(message="Shared token cache unavailable")
71+
raise ClientAuthenticationError(message="Shared token cache unavailable")
6672

6773
return self._acquire_token_silent(*scopes, **kwargs)
6874

69-
def _initialize(self):
70-
if not self._cache and platform.system() in {"Darwin", "Linux", "Windows"}:
75+
def _initialize_cache(self, is_cae: bool = False) -> Optional[TokenCache]:
76+
77+
# If no cache options were provided, the default cache will be used. This credential accepts the
78+
# user's default cache regardless of whether it's encrypted. It doesn't create a new cache. If the
79+
# default cache exists, the user must have created it earlier. If it's unencrypted, the user must
80+
# have allowed that.
81+
cache_options = self._cache_persistence_options or TokenCachePersistenceOptions(allow_unencrypted_storage=True)
82+
83+
if platform.system() not in {"Darwin", "Linux", "Windows"}:
84+
raise CredentialUnavailableError(message="Shared token cache is not supported on this platform.")
85+
86+
if not self._cache and not is_cae:
7187
try:
72-
# If no cache options were provided, the default cache will be used. This credential accepts the
73-
# user's default cache regardless of whether it's encrypted. It doesn't create a new cache. If the
74-
# default cache exists, the user must have created it earlier. If it's unencrypted, the user must
75-
# have allowed that.
76-
options = self._cache_persistence_options or TokenCachePersistenceOptions(
77-
allow_unencrypted_storage=True
78-
)
79-
self._cache = _load_persistent_cache(options)
88+
self._cache = _load_persistent_cache(cache_options, is_cae)
8089
except Exception: # pylint:disable=broad-except
81-
pass
90+
return None
8291

83-
self._initialized = True
92+
if not self._cae_cache and is_cae:
93+
try:
94+
self._cae_cache = _load_persistent_cache(cache_options, is_cae)
95+
except Exception: # pylint:disable=broad-except
96+
return None
97+
98+
return self._cae_cache if is_cae else self._cache
8499

85100
def _get_client_application(self, **kwargs: Any):
86101
tenant_id = resolve_tenant(
87102
self._tenant_id, additionally_allowed_tenants=self._additionally_allowed_tenants, **kwargs
88103
)
89-
if tenant_id not in self._client_applications:
104+
105+
client_applications_map = self._client_applications
106+
capabilities = None
107+
token_cache = self._cache
108+
109+
if kwargs.get("enable_cae"):
110+
client_applications_map = self._cae_client_applications
90111
# CP1 = can handle claims challenges (CAE)
91-
capabilities = None if EnvironmentVariables.AZURE_IDENTITY_DISABLE_CP1 in os.environ else ["CP1"]
92-
self._client_applications[tenant_id] = PublicClientApplication(
112+
capabilities = ["CP1"]
113+
token_cache = self._cae_cache
114+
115+
if tenant_id not in client_applications_map:
116+
client_applications_map[tenant_id] = PublicClientApplication(
93117
client_id=self._auth_record.client_id,
94118
authority="https://{}/{}".format(self._auth_record.authority, tenant_id),
95-
token_cache=self._cache,
119+
token_cache=token_cache,
96120
http_client=self._client,
97121
client_capabilities=capabilities,
98122
)
99-
return self._client_applications[tenant_id]
123+
return client_applications_map[tenant_id]
100124

101125
@wrap_exceptions
102126
def _acquire_token_silent(self, *scopes: str, **kwargs: Any) -> AccessToken:

sdk/identity/azure-identity/azure/identity/_internal/aad_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,4 @@ def _run_pipeline(self, request: HttpRequest, **kwargs: Any) -> AccessToken:
7070
kwargs.pop("claims", None)
7171
now = int(time.time())
7272
response = self._pipeline.run(request, retry_on_methods=self._POST, **kwargs)
73-
return self._process_response(response, now)
73+
return self._process_response(response, now, **kwargs)

0 commit comments

Comments
 (0)