Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 38 additions & 3 deletions msal/managed_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
# All rights reserved.
#
# This code is licensed under the MIT License.
import hashlib
import calendar
import datetime
import json
import logging
import os
import re
import socket
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

socket is imported but not used anywhere in this module (only referenced in a comment). Please remove the unused import to avoid lint/maintenance noise.

Suggested change
import socket

Copilot uses AI. Check for mistakes.
import hashlib
import sys
import time
from urllib.parse import urlparse # Python 3+
Expand Down Expand Up @@ -460,6 +464,37 @@ def _obtain_token(
return _obtain_token_on_azure_vm(http_client, managed_identity, resource)


def _parse_expires_on(raw: str) -> int:
try:
return int(raw) # It is typically an epoch time
except ValueError:
pass
try:
# '2024-10-18T19:51:37.0000000+00:00' was observed in
# https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/4963
if sys.version_info < (3, 11): # Does not support 7-digit microseconds
raw = re.sub( # Strip microseconds portion using regex
r'(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(\.\d+)([+-]\d{2}:\d{2})',
r'\1\3',
raw)
if raw.endswith("Z"): # fromisoformat() doesn't support Z before 3.11
raw = raw[:-1] + "+00:00"
return int(datetime.datetime.fromisoformat(raw).timestamp())
except ValueError:
pass
for format in (
"%m/%d/%Y %H:%M:%S %z", # Support "06/20/2019 02:57:58 +00:00"
# 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
"%m/%d/%Y %I:%M:%S %p %z", # Support "1/16/2020 12:0:12 AM +00:00"
# 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
):
try:
return calendar.timegm(time.strptime(raw, format))
Comment on lines +485 to +492
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loop variable name format shadows Python’s built-in format() function. Consider renaming it (e.g., fmt) to avoid shadowing and improve readability.

Suggested change
for format in (
"%m/%d/%Y %H:%M:%S %z", # Support "06/20/2019 02:57:58 +00:00"
# 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
"%m/%d/%Y %I:%M:%S %p %z", # Support "1/16/2020 12:0:12 AM +00:00"
# 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
):
try:
return calendar.timegm(time.strptime(raw, format))
for fmt in (
"%m/%d/%Y %H:%M:%S %z", # Support "06/20/2019 02:57:58 +00:00"
# 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
"%m/%d/%Y %I:%M:%S %p %z", # Support "1/16/2020 12:0:12 AM +00:00"
# 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
):
try:
return calendar.timegm(time.strptime(raw, fmt))

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

calendar.timegm(time.strptime(...)) ignores the parsed %z offset (time tuples don’t apply the timezone offset when converted this way). If expires_on ever includes a non-+00:00 offset, this will compute the wrong epoch and thus wrong expires_in. Prefer parsing into a timezone-aware datetime (e.g., datetime.datetime.strptime(...).timestamp()) so offsets are honored.

Suggested change
return calendar.timegm(time.strptime(raw, format))
return int(datetime.datetime.strptime(raw, format).timestamp())

Copilot uses AI. Check for mistakes.
except ValueError:
pass
raise ManagedIdentityError(f"Cannot parse expires_on: {raw}")
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raw is mutated during ISO preprocessing, and the final exception reports the possibly-normalized value rather than the original input. Preserve the original string (e.g., original_raw = raw) and include that in the ManagedIdentityError message to make troubleshooting easier.

Copilot uses AI. Check for mistakes.


def _adjust_param(params, managed_identity, types_mapping=None):
# Modify the params dict in place
id_name = (types_mapping or ManagedIdentity._types_mapping).get(
Expand Down Expand Up @@ -532,7 +567,7 @@ def _obtain_token_on_app_service(
if payload.get("access_token") and payload.get("expires_on"):
return { # Normalizing the payload into OAuth2 format
"access_token": payload["access_token"],
"expires_in": int(payload["expires_on"]) - int(time.time()),
"expires_in": _parse_expires_on(payload["expires_on"]) - int(time.time()),
"resource": payload.get("resource"),
"token_type": payload.get("token_type", "Bearer"),
}
Expand Down Expand Up @@ -566,7 +601,7 @@ def _obtain_token_on_machine_learning(
if payload.get("access_token") and payload.get("expires_on"):
return { # Normalizing the payload into OAuth2 format
"access_token": payload["access_token"],
"expires_in": int(payload["expires_on"]) - int(time.time()),
"expires_in": _parse_expires_on(payload["expires_on"]) - int(time.time()),
"resource": payload.get("resource"),
"token_type": payload.get("token_type", "Bearer"),
}
Expand Down
18 changes: 17 additions & 1 deletion tests/test_mi.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
MACHINE_LEARNING,
SERVICE_FABRIC,
DEFAULT_TO_VM,
_parse_expires_on,
)
from msal.token_cache import is_subdict_of

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


class ExpiresOnTestCase(unittest.TestCase):
def test_expires_on_parsing(self):
for input, epoch in {
"1234567890": 1234567890,
"1970-01-01T00:00:12.0000000+00:00": 12,
Comment on lines +59 to +61
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable name input shadows Python’s built-in input() function. Rename it (e.g., raw/expires_on) to avoid shadowing and make the test intent clearer.

Copilot uses AI. Check for mistakes.
"2024-10-18T19:51:37.0000000+00:00": 1729281097, # Copied from https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/4963
"2025-01-01T00:00:00Z": 1735689600, # Z/Zulu suffix
"2025-01-01T00:00:00+00:00": 1735689600, # No fractional seconds
"01/01/1970 00:00:12 +00:00": 12,
"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
"1/1/1970 12:0:12 AM +00:00": 12,
"1/1/1970 12:0:12 PM +00:00": 43212,
"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
}.items():
self.assertEqual(_parse_expires_on(input), epoch, f'Should parse "{input}" to {epoch}')

class ThrottledHttpClientTestCase(ThrottledHttpClientBaseTestCase):
def test_throttled_http_client_should_not_alter_original_http_client(self):
self.assertNotAlteringOriginalHttpClient(_ThrottledHttpClient)
Expand Down Expand Up @@ -83,7 +100,6 @@ def test_throttled_http_client_should_cache_unsuccessful_http_response(self):
self.assertNotEqual({}, http_cache, "Should cache unsuccessful http response")
self.assertCleanPickle(http_cache)


class ClientTestCase(unittest.TestCase):
maxDiff = None

Expand Down
Loading