Skip to content

Commit afb2fa9

Browse files
Fix Date-Time Parsing in GetDurationFromNowInSeconds for Multiple Formats (#4964)
* init * dateTimeStamp * fix * fix * pr comment --------- Co-authored-by: Gladwin Johnson <[email protected]>
1 parent d766ff1 commit afb2fa9

File tree

5 files changed

+144
-23
lines changed

5 files changed

+144
-23
lines changed

src/client/Microsoft.Identity.Client/OAuth2/MsalTokenResponse.cs

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -249,10 +249,21 @@ internal static MsalTokenResponse CreateFromiOSBrokerResponse(Dictionary<string,
249249

250250
internal static MsalTokenResponse CreateFromManagedIdentityResponse(ManagedIdentityResponse managedIdentityResponse)
251251
{
252-
ValidateManagedIdentityResult(managedIdentityResponse);
252+
// Validate that the access token is present. If it is missing, handle the error accordingly.
253+
if (string.IsNullOrEmpty(managedIdentityResponse.AccessToken))
254+
{
255+
HandleInvalidExternalValueError(nameof(managedIdentityResponse.AccessToken));
256+
}
253257

254-
long expiresIn = DateTimeHelpers.GetDurationFromNowInSeconds(managedIdentityResponse.ExpiresOn);
258+
// Parse and validate the "ExpiresOn" timestamp, which indicates when the token will expire.
259+
long expiresIn = DateTimeHelpers.GetDurationFromManagedIdentityTimestamp(managedIdentityResponse.ExpiresOn);
260+
261+
if (expiresIn <= 0)
262+
{
263+
HandleInvalidExternalValueError(nameof(managedIdentityResponse.ExpiresOn));
264+
}
255265

266+
// Construct and return an MsalTokenResponse object with the necessary details.
256267
return new MsalTokenResponse
257268
{
258269
AccessToken = managedIdentityResponse.AccessToken,
@@ -275,20 +286,6 @@ internal static MsalTokenResponse CreateFromManagedIdentityResponse(ManagedIdent
275286
return null;
276287
}
277288

278-
private static void ValidateManagedIdentityResult(ManagedIdentityResponse response)
279-
{
280-
if (string.IsNullOrEmpty(response.AccessToken))
281-
{
282-
HandleInvalidExternalValueError(nameof(response.AccessToken));
283-
}
284-
285-
long expiresIn = DateTimeHelpers.GetDurationFromNowInSeconds(response.ExpiresOn);
286-
if (expiresIn <= 0)
287-
{
288-
HandleInvalidExternalValueError(nameof(response.ExpiresOn));
289-
}
290-
}
291-
292289
internal static MsalTokenResponse CreateFromAppProviderResponse(AppTokenProviderResult tokenProviderResponse)
293290
{
294291
ValidateTokenProviderResult(tokenProviderResponse);

src/client/Microsoft.Identity.Client/Utils/DateTimeHelpers.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,31 @@ public static long GetDurationFromNowInSeconds(string unixTimestampInFuture)
7474
return expiresOnUnixTimestamp - CurrDateTimeInUnixTimestamp();
7575
}
7676

77+
public static long GetDurationFromManagedIdentityTimestamp(string dateTimeStamp)
78+
{
79+
if (string.IsNullOrEmpty(dateTimeStamp))
80+
{
81+
return 0;
82+
}
83+
84+
// First, try to parse as Unix timestamp (number of seconds since epoch)
85+
// Example: "1697490590" (Unix timestamp representing seconds since 1970-01-01)
86+
if (long.TryParse(dateTimeStamp, out long expiresOnUnixTimestamp))
87+
{
88+
return expiresOnUnixTimestamp - DateTimeHelpers.CurrDateTimeInUnixTimestamp();
89+
}
90+
91+
// Try parsing as ISO 8601
92+
// Example: "2024-10-18T19:51:37.0000000+00:00" (ISO 8601 format)
93+
if (DateTimeOffset.TryParse(dateTimeStamp, null, DateTimeStyles.RoundtripKind, out DateTimeOffset expiresOnDateTime))
94+
{
95+
return (long)(expiresOnDateTime - DateTimeOffset.UtcNow).TotalSeconds;
96+
}
97+
98+
// If no format works, throw an MSAL client exception
99+
throw new MsalClientException("invalid_timestamp_format", $"Failed to parse date-time stamp from identity provider. Invalid format: '{dateTimeStamp}'.");
100+
}
101+
77102
public static DateTimeOffset? DateTimeOffsetFromDuration(long? duration)
78103
{
79104
if (duration.HasValue)

tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,21 @@ public static string GetBridgedHybridSpaTokenResponse(string spaAccountId)
113113
",\"id_token_expires_in\":\"3600\"}";
114114
}
115115

116-
public static string GetMsiSuccessfulResponse(int expiresInHours = 1)
116+
public static string GetMsiSuccessfulResponse(int expiresInHours = 1, bool useIsoFormat = false)
117117
{
118-
string expiresOn = DateTimeHelpers.DateTimeToUnixTimestamp(DateTime.UtcNow.AddHours(expiresInHours));
118+
string expiresOn;
119+
120+
if (useIsoFormat)
121+
{
122+
// Return ISO 8601 format
123+
expiresOn = DateTime.UtcNow.AddHours(expiresInHours).ToString("o", CultureInfo.InvariantCulture);
124+
}
125+
else
126+
{
127+
// Return Unix timestamp format
128+
expiresOn = DateTimeHelpers.DateTimeToUnixTimestamp(DateTime.UtcNow.AddHours(expiresInHours));
129+
}
130+
119131
return
120132
"{\"access_token\":\"" + TestConstants.ATSecret + "\",\"expires_on\":\"" + expiresOn + "\",\"resource\":\"https://management.azure.com/\",\"token_type\":" +
121133
"\"Bearer\",\"client_id\":\"client_id\"}";

tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -806,10 +806,13 @@ public async Task ManagedIdentityCacheTestAsync()
806806
}
807807

808808
[DataTestMethod]
809-
[DataRow(1, false)]
810-
[DataRow(2, false)]
811-
[DataRow(3, true)]
812-
public async Task ManagedIdentityExpiresOnTestAsync(int expiresInHours, bool refreshOnHasValue)
809+
[DataRow(1, false, false)] // Unix timestamp
810+
[DataRow(2, false, false)] // Unix timestamp
811+
[DataRow(3, true, false)] // Unix timestamp
812+
[DataRow(1, false, true)] // ISO 8601
813+
[DataRow(2, false, true)] // ISO 8601
814+
[DataRow(3, true, true)] // ISO 8601
815+
public async Task ManagedIdentityExpiresOnTestAsync(int expiresInHours, bool refreshOnHasValue, bool useIsoFormat)
813816
{
814817
using (new EnvVariableContext())
815818
using (var httpManager = new MockHttpManager(isManagedIdentity: true))
@@ -827,7 +830,7 @@ public async Task ManagedIdentityExpiresOnTestAsync(int expiresInHours, bool ref
827830
httpManager.AddManagedIdentityMockHandler(
828831
AppServiceEndpoint,
829832
Resource,
830-
MockHelpers.GetMsiSuccessfulResponse(expiresInHours),
833+
MockHelpers.GetMsiSuccessfulResponse(expiresInHours, useIsoFormat),
831834
ManagedIdentitySource.AppService);
832835

833836
AcquireTokenForManagedIdentityParameterBuilder builder = mi.AcquireTokenForManagedIdentity(Resource);
@@ -838,6 +841,8 @@ public async Task ManagedIdentityExpiresOnTestAsync(int expiresInHours, bool ref
838841
Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource);
839842
Assert.AreEqual(ApiEvent.ApiIds.AcquireTokenForSystemAssignedManagedIdentity, builder.CommonParameters.ApiId);
840843
Assert.AreEqual(refreshOnHasValue, result.AuthenticationResultMetadata.RefreshOn.HasValue);
844+
Assert.IsTrue(result.ExpiresOn > DateTimeOffset.UtcNow, "The token's ExpiresOn should be in the future.");
845+
841846
}
842847
}
843848

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Globalization;
6+
using System.Linq;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
using Microsoft.Identity.Client;
10+
using Microsoft.Identity.Client.Utils;
11+
using Microsoft.Identity.Test.Common;
12+
using Microsoft.Identity.Test.Common.Core.Helpers;
13+
using Microsoft.VisualStudio.TestTools.UnitTesting;
14+
15+
namespace Microsoft.Identity.Test.Unit.UtilTests
16+
{
17+
[TestClass]
18+
public class DateTimeHelperTests
19+
{
20+
[TestMethod]
21+
public void TestGetDurationFromNowInSecondsForUnixTimestampOnly()
22+
{
23+
// Arrange
24+
var currentTime = DateTimeOffset.UtcNow;
25+
26+
// Example 1: Valid Unix timestamp (seconds since epoch)
27+
long currentUnixTimestamp = DateTimeHelpers.CurrDateTimeInUnixTimestamp(); // e.g., 1697490590
28+
string unixTimestampString = currentUnixTimestamp.ToString(CultureInfo.InvariantCulture);
29+
long result = DateTimeHelpers.GetDurationFromNowInSeconds(unixTimestampString);
30+
Assert.IsTrue(result >= 0, "Valid Unix timestamp (seconds) failed");
31+
32+
// Example 2: Unix timestamp in the future
33+
string futureUnixTimestamp = (currentUnixTimestamp + 3600).ToString(); // 1 hour from now
34+
result = DateTimeHelpers.GetDurationFromNowInSeconds(futureUnixTimestamp);
35+
Assert.IsTrue(result > 0, "Future Unix timestamp failed");
36+
37+
// Example 3: Unix timestamp in the past
38+
string pastUnixTimestamp = (currentUnixTimestamp - 3600).ToString(); // 1 hour ago
39+
result = DateTimeHelpers.GetDurationFromNowInSeconds(pastUnixTimestamp);
40+
Assert.IsTrue(result < 0, "Past Unix timestamp failed");
41+
42+
// Example 4: Empty string (should return 0)
43+
string emptyString = string.Empty;
44+
result = DateTimeHelpers.GetDurationFromNowInSeconds(emptyString);
45+
Assert.AreEqual(0, result, "Empty string did not return 0 as expected.");
46+
}
47+
48+
[TestMethod]
49+
public void TestGetDurationFromNowInSecondsFromManagedIdentity()
50+
{
51+
// Arrange
52+
var currentTime = DateTimeOffset.UtcNow;
53+
54+
// Example 1: Unix timestamp (seconds since epoch)
55+
string unixTimestampInSeconds = DateTimeHelpers.DateTimeToUnixTimestamp(currentTime); // e.g., 1697490590
56+
long result = DateTimeHelpers.GetDurationFromManagedIdentityTimestamp(unixTimestampInSeconds);
57+
Assert.IsTrue(result >= 0, "Unix timestamp (seconds) failed");
58+
59+
// Example 2: ISO 8601 format
60+
string iso8601 = currentTime.ToString("o", CultureInfo.InvariantCulture); // e.g., 2024-10-18T19:51:37.0000000+00:00
61+
result = DateTimeHelpers.GetDurationFromManagedIdentityTimestamp(iso8601);
62+
Assert.IsTrue(result >= 0, "ISO 8601 failed");
63+
64+
// Example 3: Common format (MM/dd/yyyy HH:mm:ss)
65+
string commonFormat1 = currentTime.ToString("MM/dd/yyyy HH:mm:ss", CultureInfo.InvariantCulture); // e.g., 10/18/2024 19:51:37
66+
result = DateTimeHelpers.GetDurationFromManagedIdentityTimestamp(commonFormat1);
67+
Assert.IsTrue(result >= 0, "Common Format 1 failed");
68+
69+
// Example 4: Common format (yyyy-MM-dd HH:mm:ss)
70+
string commonFormat2 = currentTime.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); // e.g., 2024-10-18 19:51:37
71+
result = DateTimeHelpers.GetDurationFromManagedIdentityTimestamp(commonFormat2);
72+
Assert.IsTrue(result >= 0, "Common Format 2 failed");
73+
74+
// Example 5: Invalid format (should throw an MsalClientException)
75+
string invalidFormat = "invalid-date-format";
76+
Assert.ThrowsException<MsalClientException>(() =>
77+
{
78+
DateTimeHelpers.GetDurationFromManagedIdentityTimestamp(invalidFormat);
79+
}, "Invalid format did not throw an exception as expected.");
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)