Skip to content

Commit

Permalink
Fix Date-Time Parsing in GetDurationFromNowInSeconds for Multiple For…
Browse files Browse the repository at this point in the history
…mats (#4964)

* init

* dateTimeStamp

* fix

* fix

* pr comment

---------

Co-authored-by: Gladwin Johnson <[email protected]>
  • Loading branch information
gladjohn and GladwinJohnson authored Oct 22, 2024
1 parent d766ff1 commit afb2fa9
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 23 deletions.
29 changes: 13 additions & 16 deletions src/client/Microsoft.Identity.Client/OAuth2/MsalTokenResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -249,10 +249,21 @@ internal static MsalTokenResponse CreateFromiOSBrokerResponse(Dictionary<string,

internal static MsalTokenResponse CreateFromManagedIdentityResponse(ManagedIdentityResponse managedIdentityResponse)
{
ValidateManagedIdentityResult(managedIdentityResponse);
// Validate that the access token is present. If it is missing, handle the error accordingly.
if (string.IsNullOrEmpty(managedIdentityResponse.AccessToken))
{
HandleInvalidExternalValueError(nameof(managedIdentityResponse.AccessToken));
}

long expiresIn = DateTimeHelpers.GetDurationFromNowInSeconds(managedIdentityResponse.ExpiresOn);
// Parse and validate the "ExpiresOn" timestamp, which indicates when the token will expire.
long expiresIn = DateTimeHelpers.GetDurationFromManagedIdentityTimestamp(managedIdentityResponse.ExpiresOn);

if (expiresIn <= 0)
{
HandleInvalidExternalValueError(nameof(managedIdentityResponse.ExpiresOn));
}

// Construct and return an MsalTokenResponse object with the necessary details.
return new MsalTokenResponse
{
AccessToken = managedIdentityResponse.AccessToken,
Expand All @@ -275,20 +286,6 @@ internal static MsalTokenResponse CreateFromManagedIdentityResponse(ManagedIdent
return null;
}

private static void ValidateManagedIdentityResult(ManagedIdentityResponse response)
{
if (string.IsNullOrEmpty(response.AccessToken))
{
HandleInvalidExternalValueError(nameof(response.AccessToken));
}

long expiresIn = DateTimeHelpers.GetDurationFromNowInSeconds(response.ExpiresOn);
if (expiresIn <= 0)
{
HandleInvalidExternalValueError(nameof(response.ExpiresOn));
}
}

internal static MsalTokenResponse CreateFromAppProviderResponse(AppTokenProviderResult tokenProviderResponse)
{
ValidateTokenProviderResult(tokenProviderResponse);
Expand Down
25 changes: 25 additions & 0 deletions src/client/Microsoft.Identity.Client/Utils/DateTimeHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,31 @@ public static long GetDurationFromNowInSeconds(string unixTimestampInFuture)
return expiresOnUnixTimestamp - CurrDateTimeInUnixTimestamp();
}

public static long GetDurationFromManagedIdentityTimestamp(string dateTimeStamp)
{
if (string.IsNullOrEmpty(dateTimeStamp))
{
return 0;
}

// First, try to parse as Unix timestamp (number of seconds since epoch)
// Example: "1697490590" (Unix timestamp representing seconds since 1970-01-01)
if (long.TryParse(dateTimeStamp, out long expiresOnUnixTimestamp))
{
return expiresOnUnixTimestamp - DateTimeHelpers.CurrDateTimeInUnixTimestamp();
}

// Try parsing as ISO 8601
// Example: "2024-10-18T19:51:37.0000000+00:00" (ISO 8601 format)
if (DateTimeOffset.TryParse(dateTimeStamp, null, DateTimeStyles.RoundtripKind, out DateTimeOffset expiresOnDateTime))
{
return (long)(expiresOnDateTime - DateTimeOffset.UtcNow).TotalSeconds;
}

// If no format works, throw an MSAL client exception
throw new MsalClientException("invalid_timestamp_format", $"Failed to parse date-time stamp from identity provider. Invalid format: '{dateTimeStamp}'.");
}

public static DateTimeOffset? DateTimeOffsetFromDuration(long? duration)
{
if (duration.HasValue)
Expand Down
16 changes: 14 additions & 2 deletions tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,21 @@ public static string GetBridgedHybridSpaTokenResponse(string spaAccountId)
",\"id_token_expires_in\":\"3600\"}";
}

public static string GetMsiSuccessfulResponse(int expiresInHours = 1)
public static string GetMsiSuccessfulResponse(int expiresInHours = 1, bool useIsoFormat = false)
{
string expiresOn = DateTimeHelpers.DateTimeToUnixTimestamp(DateTime.UtcNow.AddHours(expiresInHours));
string expiresOn;

if (useIsoFormat)
{
// Return ISO 8601 format
expiresOn = DateTime.UtcNow.AddHours(expiresInHours).ToString("o", CultureInfo.InvariantCulture);
}
else
{
// Return Unix timestamp format
expiresOn = DateTimeHelpers.DateTimeToUnixTimestamp(DateTime.UtcNow.AddHours(expiresInHours));
}

return
"{\"access_token\":\"" + TestConstants.ATSecret + "\",\"expires_on\":\"" + expiresOn + "\",\"resource\":\"https://management.azure.com/\",\"token_type\":" +
"\"Bearer\",\"client_id\":\"client_id\"}";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -806,10 +806,13 @@ public async Task ManagedIdentityCacheTestAsync()
}

[DataTestMethod]
[DataRow(1, false)]
[DataRow(2, false)]
[DataRow(3, true)]
public async Task ManagedIdentityExpiresOnTestAsync(int expiresInHours, bool refreshOnHasValue)
[DataRow(1, false, false)] // Unix timestamp
[DataRow(2, false, false)] // Unix timestamp
[DataRow(3, true, false)] // Unix timestamp
[DataRow(1, false, true)] // ISO 8601
[DataRow(2, false, true)] // ISO 8601
[DataRow(3, true, true)] // ISO 8601
public async Task ManagedIdentityExpiresOnTestAsync(int expiresInHours, bool refreshOnHasValue, bool useIsoFormat)
{
using (new EnvVariableContext())
using (var httpManager = new MockHttpManager(isManagedIdentity: true))
Expand All @@ -827,7 +830,7 @@ public async Task ManagedIdentityExpiresOnTestAsync(int expiresInHours, bool ref
httpManager.AddManagedIdentityMockHandler(
AppServiceEndpoint,
Resource,
MockHelpers.GetMsiSuccessfulResponse(expiresInHours),
MockHelpers.GetMsiSuccessfulResponse(expiresInHours, useIsoFormat),
ManagedIdentitySource.AppService);

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

}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Utils;
using Microsoft.Identity.Test.Common;
using Microsoft.Identity.Test.Common.Core.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Microsoft.Identity.Test.Unit.UtilTests
{
[TestClass]
public class DateTimeHelperTests
{
[TestMethod]
public void TestGetDurationFromNowInSecondsForUnixTimestampOnly()
{
// Arrange
var currentTime = DateTimeOffset.UtcNow;

// Example 1: Valid Unix timestamp (seconds since epoch)
long currentUnixTimestamp = DateTimeHelpers.CurrDateTimeInUnixTimestamp(); // e.g., 1697490590
string unixTimestampString = currentUnixTimestamp.ToString(CultureInfo.InvariantCulture);
long result = DateTimeHelpers.GetDurationFromNowInSeconds(unixTimestampString);
Assert.IsTrue(result >= 0, "Valid Unix timestamp (seconds) failed");

// Example 2: Unix timestamp in the future
string futureUnixTimestamp = (currentUnixTimestamp + 3600).ToString(); // 1 hour from now
result = DateTimeHelpers.GetDurationFromNowInSeconds(futureUnixTimestamp);
Assert.IsTrue(result > 0, "Future Unix timestamp failed");

// Example 3: Unix timestamp in the past
string pastUnixTimestamp = (currentUnixTimestamp - 3600).ToString(); // 1 hour ago
result = DateTimeHelpers.GetDurationFromNowInSeconds(pastUnixTimestamp);
Assert.IsTrue(result < 0, "Past Unix timestamp failed");

// Example 4: Empty string (should return 0)
string emptyString = string.Empty;
result = DateTimeHelpers.GetDurationFromNowInSeconds(emptyString);
Assert.AreEqual(0, result, "Empty string did not return 0 as expected.");
}

[TestMethod]
public void TestGetDurationFromNowInSecondsFromManagedIdentity()
{
// Arrange
var currentTime = DateTimeOffset.UtcNow;

// Example 1: Unix timestamp (seconds since epoch)
string unixTimestampInSeconds = DateTimeHelpers.DateTimeToUnixTimestamp(currentTime); // e.g., 1697490590
long result = DateTimeHelpers.GetDurationFromManagedIdentityTimestamp(unixTimestampInSeconds);
Assert.IsTrue(result >= 0, "Unix timestamp (seconds) failed");

// Example 2: ISO 8601 format
string iso8601 = currentTime.ToString("o", CultureInfo.InvariantCulture); // e.g., 2024-10-18T19:51:37.0000000+00:00
result = DateTimeHelpers.GetDurationFromManagedIdentityTimestamp(iso8601);
Assert.IsTrue(result >= 0, "ISO 8601 failed");

// Example 3: Common format (MM/dd/yyyy HH:mm:ss)
string commonFormat1 = currentTime.ToString("MM/dd/yyyy HH:mm:ss", CultureInfo.InvariantCulture); // e.g., 10/18/2024 19:51:37
result = DateTimeHelpers.GetDurationFromManagedIdentityTimestamp(commonFormat1);
Assert.IsTrue(result >= 0, "Common Format 1 failed");

// Example 4: Common format (yyyy-MM-dd HH:mm:ss)
string commonFormat2 = currentTime.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); // e.g., 2024-10-18 19:51:37
result = DateTimeHelpers.GetDurationFromManagedIdentityTimestamp(commonFormat2);
Assert.IsTrue(result >= 0, "Common Format 2 failed");

// Example 5: Invalid format (should throw an MsalClientException)
string invalidFormat = "invalid-date-format";
Assert.ThrowsException<MsalClientException>(() =>
{
DateTimeHelpers.GetDurationFromManagedIdentityTimestamp(invalidFormat);
}, "Invalid format did not throw an exception as expected.");
}
}
}

0 comments on commit afb2fa9

Please sign in to comment.