Skip to content

Commit 501cb2c

Browse files
authored
[Identity] Support expires_on in AzureCLICredential (#33947)
Newer versions of Azure CLI now also return a Unix timestamp with the `expires_on` field when retrieving an access token. We should prefer using that. Signed-off-by: Paul Van Eck <[email protected]>
1 parent 370ecaa commit 501cb2c

File tree

4 files changed

+94
-8
lines changed

4 files changed

+94
-8
lines changed

sdk/identity/azure-identity/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
### Other Changes
1515

16+
- `AzureCliCredential` utilizes the new `expires_on` property returned by `az` CLI versions >= 2.54.0 to determine token expiration. ([#33947](https://github.com/Azure/azure-sdk-for-python/issues/33947))
17+
1618
## 1.15.0 (2023-10-26)
1719

1820
### Features Added

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import shutil
1010
import subprocess
1111
import sys
12-
import time
1312
from typing import List, Optional, Any, Dict
1413

1514
from azure.core.credentials import AccessToken
@@ -139,14 +138,13 @@ def parse_token(output) -> Optional[AccessToken]:
139138
"""
140139
try:
141140
token = json.loads(output)
142-
dt = datetime.strptime(token["expiresOn"], "%Y-%m-%d %H:%M:%S.%f")
143-
if hasattr(dt, "timestamp"):
144-
# Python >= 3.3
145-
expires_on = dt.timestamp()
146-
else:
147-
# taken from Python 3.5's datetime.timestamp()
148-
expires_on = time.mktime((dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, -1, -1, -1))
149141

142+
# Use "expires_on" if it's present, otherwise use "expiresOn".
143+
if "expires_on" in token:
144+
return AccessToken(token["accessToken"], int(token["expires_on"]))
145+
146+
dt = datetime.strptime(token["expiresOn"], "%Y-%m-%d %H:%M:%S.%f")
147+
expires_on = dt.timestamp()
150148
return AccessToken(token["accessToken"], int(expires_on))
151149
except (KeyError, ValueError):
152150
return None

sdk/identity/azure-identity/tests/test_cli_credential.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,48 @@ def test_get_token():
9292
assert token.expires_on == expected_expires_on
9393

9494

95+
def test_expires_on_used():
96+
"""Test that 'expires_on' is preferred over 'expiresOn'."""
97+
expires_on = 1602015811
98+
successful_output = json.dumps(
99+
{
100+
"expiresOn": datetime.fromtimestamp(1555555555).strftime("%Y-%m-%d %H:%M:%S.%f"),
101+
"expires_on": expires_on,
102+
"accessToken": "access token",
103+
"subscription": "some-guid",
104+
"tenant": "some-guid",
105+
"tokenType": "Bearer",
106+
}
107+
)
108+
109+
with mock.patch("shutil.which", return_value="az"):
110+
with mock.patch(CHECK_OUTPUT, mock.Mock(return_value=successful_output)):
111+
token = AzureCliCredential().get_token("scope")
112+
113+
assert token.expires_on == expires_on
114+
115+
116+
def test_expires_on_string():
117+
"""Test that 'expires_on' still works if it's a string."""
118+
expires_on = 1602015811
119+
successful_output = json.dumps(
120+
{
121+
"expires_on": f"{expires_on}",
122+
"accessToken": "access token",
123+
"subscription": "some-guid",
124+
"tenant": "some-guid",
125+
"tokenType": "Bearer",
126+
}
127+
)
128+
129+
with mock.patch("shutil.which", return_value="az"):
130+
with mock.patch(CHECK_OUTPUT, mock.Mock(return_value=successful_output)):
131+
token = AzureCliCredential().get_token("scope")
132+
133+
assert type(token.expires_on) == int
134+
assert token.expires_on == expires_on
135+
136+
95137
def test_cli_not_installed():
96138
"""The credential should raise CredentialUnavailableError when the CLI isn't installed"""
97139
with mock.patch("shutil.which", return_value=None):

sdk/identity/azure-identity/tests/test_cli_credential_async.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,50 @@ async def test_get_token():
119119
assert token.expires_on == expected_expires_on
120120

121121

122+
async def test_expires_on_used():
123+
"""Test that 'expires_on' is preferred over 'expiresOn'."""
124+
expires_on = 1602015811
125+
successful_output = json.dumps(
126+
{
127+
"expiresOn": datetime.fromtimestamp(1555555555).strftime("%Y-%m-%d %H:%M:%S.%f"),
128+
"expires_on": expires_on,
129+
"accessToken": "access token",
130+
"subscription": "some-guid",
131+
"tenant": "some-guid",
132+
"tokenType": "Bearer",
133+
}
134+
)
135+
136+
with mock.patch("shutil.which", return_value="az"):
137+
with mock.patch(SUBPROCESS_EXEC, mock_exec(successful_output)):
138+
credential = AzureCliCredential()
139+
token = await credential.get_token("scope")
140+
141+
assert token.expires_on == expires_on
142+
143+
144+
async def test_expires_on_string():
145+
"""Test that 'expires_on' still works if it's a string."""
146+
expires_on = 1602015811
147+
successful_output = json.dumps(
148+
{
149+
"expires_on": f"{expires_on}",
150+
"accessToken": "access token",
151+
"subscription": "some-guid",
152+
"tenant": "some-guid",
153+
"tokenType": "Bearer",
154+
}
155+
)
156+
157+
with mock.patch("shutil.which", return_value="az"):
158+
with mock.patch(SUBPROCESS_EXEC, mock_exec(successful_output)):
159+
credential = AzureCliCredential()
160+
token = await credential.get_token("scope")
161+
162+
assert type(token.expires_on) == int
163+
assert token.expires_on == expires_on
164+
165+
122166
async def test_cli_not_installed():
123167
"""The credential should raise CredentialUnavailableError when the CLI isn't installed"""
124168

0 commit comments

Comments
 (0)