Skip to content

Commit 12cf1bf

Browse files
Copilotgladjohn
andauthored
Support ISO8601-ish format on expires_on in MI (recreates PR #804)
Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-for-python/sessions/988c4f0a-9dee-4865-9c61-1dccdfd9ea49 Co-authored-by: gladjohn <90415114+gladjohn@users.noreply.github.com>
1 parent 5989502 commit 12cf1bf

File tree

2 files changed

+55
-4
lines changed

2 files changed

+55
-4
lines changed

msal/managed_identity.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
# All rights reserved.
33
#
44
# This code is licensed under the MIT License.
5-
import hashlib
5+
import calendar
6+
import datetime
67
import json
78
import logging
89
import os
10+
import re
11+
import socket
12+
import hashlib
913
import sys
1014
import time
1115
from urllib.parse import urlparse # Python 3+
@@ -460,6 +464,37 @@ def _obtain_token(
460464
return _obtain_token_on_azure_vm(http_client, managed_identity, resource)
461465

462466

467+
def _parse_expires_on(raw: str) -> int:
468+
try:
469+
return int(raw) # It is typically an epoch time
470+
except ValueError:
471+
pass
472+
try:
473+
# '2024-10-18T19:51:37.0000000+00:00' was observed in
474+
# https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/4963
475+
if sys.version_info < (3, 11): # Does not support 7-digit microseconds
476+
raw = re.sub( # Strip microseconds portion using regex
477+
r'(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(\.\d+)([+-]\d{2}:\d{2})',
478+
r'\1\3',
479+
raw)
480+
if raw.endswith("Z"): # fromisoformat() doesn't support Z before 3.11
481+
raw = raw[:-1] + "+00:00"
482+
return int(datetime.datetime.fromisoformat(raw).timestamp())
483+
except ValueError:
484+
pass
485+
for format in (
486+
"%m/%d/%Y %H:%M:%S %z", # Support "06/20/2019 02:57:58 +00:00"
487+
# Derived from https://github.com/Azure/azure-sdk-for-python/blob/azure-identity_1.21.0/sdk/identity/azure-identity/azure/identity/_credentials/azure_ml.py#L52
488+
"%m/%d/%Y %I:%M:%S %p %z", # Support "1/16/2020 12:0:12 AM +00:00"
489+
# Derived from https://github.com/Azure/azure-sdk-for-python/blob/azure-identity_1.21.0/sdk/identity/azure-identity/azure/identity/_credentials/azure_ml.py#L51
490+
):
491+
try:
492+
return calendar.timegm(time.strptime(raw, format))
493+
except ValueError:
494+
pass
495+
raise ManagedIdentityError(f"Cannot parse expires_on: {raw}")
496+
497+
463498
def _adjust_param(params, managed_identity, types_mapping=None):
464499
# Modify the params dict in place
465500
id_name = (types_mapping or ManagedIdentity._types_mapping).get(
@@ -532,7 +567,7 @@ def _obtain_token_on_app_service(
532567
if payload.get("access_token") and payload.get("expires_on"):
533568
return { # Normalizing the payload into OAuth2 format
534569
"access_token": payload["access_token"],
535-
"expires_in": int(payload["expires_on"]) - int(time.time()),
570+
"expires_in": _parse_expires_on(payload["expires_on"]) - int(time.time()),
536571
"resource": payload.get("resource"),
537572
"token_type": payload.get("token_type", "Bearer"),
538573
}
@@ -566,7 +601,7 @@ def _obtain_token_on_machine_learning(
566601
if payload.get("access_token") and payload.get("expires_on"):
567602
return { # Normalizing the payload into OAuth2 format
568603
"access_token": payload["access_token"],
569-
"expires_in": int(payload["expires_on"]) - int(time.time()),
604+
"expires_in": _parse_expires_on(payload["expires_on"]) - int(time.time()),
570605
"resource": payload.get("resource"),
571606
"token_type": payload.get("token_type", "Bearer"),
572607
}

tests/test_mi.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
MACHINE_LEARNING,
3030
SERVICE_FABRIC,
3131
DEFAULT_TO_VM,
32+
_parse_expires_on,
3233
)
3334
from msal.token_cache import is_subdict_of
3435

@@ -53,6 +54,22 @@ def test_helper_class_should_be_interchangable_with_dict_which_could_be_loaded_f
5354
{"ManagedIdentityIdType": "SystemAssigned", "Id": None})
5455

5556

57+
class ExpiresOnTestCase(unittest.TestCase):
58+
def test_expires_on_parsing(self):
59+
for input, epoch in {
60+
"1234567890": 1234567890,
61+
"1970-01-01T00:00:12.0000000+00:00": 12,
62+
"2024-10-18T19:51:37.0000000+00:00": 1729281097, # Copied from https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/4963
63+
"2025-01-01T00:00:00Z": 1735689600, # Z/Zulu suffix
64+
"2025-01-01T00:00:00+00:00": 1735689600, # No fractional seconds
65+
"01/01/1970 00:00:12 +00:00": 12,
66+
"06/20/2019 02:57:58 +00:00": 1560999478, # Derived from https://github.com/Azure/azure-sdk-for-python/blob/azure-identity_1.21.0/sdk/identity/azure-identity/azure/identity/_credentials/azure_ml.py#L52
67+
"1/1/1970 12:0:12 AM +00:00": 12,
68+
"1/1/1970 12:0:12 PM +00:00": 43212,
69+
"1/16/2020 5:24:12 AM +00:00": 1579152252, # Derived from https://github.com/Azure/azure-sdk-for-python/blob/azure-identity_1.21.0/sdk/identity/azure-identity/azure/identity/_credentials/azure_ml.py#L51
70+
}.items():
71+
self.assertEqual(_parse_expires_on(input), epoch, f'Should parse "{input}" to {epoch}')
72+
5673
class ThrottledHttpClientTestCase(ThrottledHttpClientBaseTestCase):
5774
def test_throttled_http_client_should_not_alter_original_http_client(self):
5875
self.assertNotAlteringOriginalHttpClient(_ThrottledHttpClient)
@@ -83,7 +100,6 @@ def test_throttled_http_client_should_cache_unsuccessful_http_response(self):
83100
self.assertNotEqual({}, http_cache, "Should cache unsuccessful http response")
84101
self.assertCleanPickle(http_cache)
85102

86-
87103
class ClientTestCase(unittest.TestCase):
88104
maxDiff = None
89105

0 commit comments

Comments
 (0)