diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index f9efb3545892..2edd709843cb 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -1,5 +1,11 @@ # Release History +## 1.4.1 (2020-10-07) +### Fixed +- `AzureCliCredential.get_token` correctly sets token expiration time, + preventing clients from using expired tokens + ([#14345](https://github.com/Azure/azure-sdk-for-python/issues/14345)) + ## 1.4.0 (2020-08-10) ### Added - `DefaultAzureCredential` uses the value of environment variable diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py index 8e609d83ff05..24d7a48577d1 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py @@ -8,6 +8,7 @@ import platform import re import sys +import time from typing import TYPE_CHECKING import subprocess @@ -68,14 +69,18 @@ def get_token(self, *scopes, **kwargs): # pylint:disable=no-self-use,unused-arg def parse_token(output): """Parse output of 'az account get-access-token' to an AccessToken. - In particular, convert the CLI's "expiresOn" value, the string representation of a naive datetime, to epoch seconds. + In particular, convert the "expiresOn" value to epoch seconds. This value is a naive local datetime as returned by + datetime.fromtimestamp. """ try: token = json.loads(output) - parsed_expires_on = datetime.strptime(token["expiresOn"], "%Y-%m-%d %H:%M:%S.%f") - - # calculate seconds since the epoch; parsed_expires_on is naive - expires_on = (parsed_expires_on - datetime.fromtimestamp(0)).total_seconds() + dt = datetime.strptime(token["expiresOn"], "%Y-%m-%d %H:%M:%S.%f") + if hasattr(dt, "timestamp"): + # Python >= 3.3 + expires_on = dt.timestamp() + else: + # taken from Python 3.5's datetime.timestamp() + expires_on = time.mktime((dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, -1, -1, -1)) return AccessToken(token["accessToken"], int(expires_on)) except (KeyError, ValueError): diff --git a/sdk/identity/azure-identity/azure/identity/_version.py b/sdk/identity/azure-identity/azure/identity/_version.py index c374812dc6f3..a5b3f7bf1cb2 100644 --- a/sdk/identity/azure-identity/azure/identity/_version.py +++ b/sdk/identity/azure-identity/azure/identity/_version.py @@ -2,4 +2,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -VERSION = "1.4.0" +VERSION = "1.4.1" diff --git a/sdk/identity/azure-identity/tests/test_cli_credential.py b/sdk/identity/azure-identity/tests/test_cli_credential.py index 3467a451356a..fc94ce97f931 100644 --- a/sdk/identity/azure-identity/tests/test_cli_credential.py +++ b/sdk/identity/azure-identity/tests/test_cli_credential.py @@ -51,11 +51,10 @@ def test_get_token(): """The credential should parse the CLI's output to an AccessToken""" access_token = "access token" - valid_seconds = 42 + expected_expires_on = 1602015811 successful_output = json.dumps( { - # expiresOn is a naive datetime representing valid_seconds from the epoch - "expiresOn": datetime.fromtimestamp(valid_seconds).strftime("%Y-%m-%d %H:%M:%S.%f"), + "expiresOn": datetime.fromtimestamp(expected_expires_on).strftime("%Y-%m-%d %H:%M:%S.%f"), "accessToken": access_token, "subscription": "some-guid", "tenant": "some-guid", @@ -68,7 +67,7 @@ def test_get_token(): assert token.token == access_token assert type(token.expires_on) == int - assert token.expires_on == valid_seconds + assert token.expires_on == expected_expires_on def test_cli_not_installed_linux(): diff --git a/sdk/identity/azure-identity/tests/test_cli_credential_async.py b/sdk/identity/azure-identity/tests/test_cli_credential_async.py index ae9dec90a341..947d5370d4a6 100644 --- a/sdk/identity/azure-identity/tests/test_cli_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_cli_credential_async.py @@ -75,11 +75,10 @@ async def test_get_token(): """The credential should parse the CLI's output to an AccessToken""" access_token = "access token" - valid_seconds = 42 + expected_expires_on = 1602015811 successful_output = json.dumps( { - # expiresOn is a naive datetime representing valid_seconds from the epoch - "expiresOn": datetime.fromtimestamp(valid_seconds).strftime("%Y-%m-%d %H:%M:%S.%f"), + "expiresOn": datetime.fromtimestamp(expected_expires_on).strftime("%Y-%m-%d %H:%M:%S.%f"), "accessToken": access_token, "subscription": "some-guid", "tenant": "some-guid", @@ -93,7 +92,7 @@ async def test_get_token(): assert token.token == access_token assert type(token.expires_on) == int - assert token.expires_on == valid_seconds + assert token.expires_on == expected_expires_on async def test_cli_not_installed_linux():