Skip to content

Commit 9dddd49

Browse files
authored
Merge pull request #956 from AzureAD/avdunn/date-fix
Support multiple date formats in managed identity flows
2 parents ed7619e + e1c7634 commit 9dddd49

File tree

4 files changed

+152
-7
lines changed

4 files changed

+152
-7
lines changed

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByManagedIdentitySupplier.java

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.slf4j.Logger;
77
import org.slf4j.LoggerFactory;
88

9+
import java.time.Instant;
910
import java.util.HashSet;
1011
import java.util.Set;
1112

@@ -115,14 +116,13 @@ private AuthenticationResult fetchNewAccessTokenAndSaveToCache(TokenRequestExecu
115116

116117
AuthenticationResult authenticationResult = createFromManagedIdentityResponse(managedIdentityResponse);
117118
clientApplication.tokenCache.saveTokens(tokenRequestExecutor, authenticationResult, clientApplication.authenticationAuthority.host);
118-
AuthenticationResult result = authenticationResult;
119-
result.metadata().tokenSource(TokenSource.IDENTITY_PROVIDER);
120-
result.metadata().cacheRefreshReason(cacheRefreshReason);
121-
return result;
119+
authenticationResult.metadata().tokenSource(TokenSource.IDENTITY_PROVIDER);
120+
authenticationResult.metadata().cacheRefreshReason(cacheRefreshReason);
121+
return authenticationResult;
122122
}
123123

124124
private AuthenticationResult createFromManagedIdentityResponse(ManagedIdentityResponse managedIdentityResponse) {
125-
long expiresOn = Long.parseLong(managedIdentityResponse.expiresOn);
125+
long expiresOn = getExpiresOnFromManagedIdentityTimestamp(managedIdentityResponse.expiresOn);
126126
long refreshOn = calculateRefreshOn(expiresOn);
127127
AuthenticationResultMetadata metadata = AuthenticationResultMetadata.builder()
128128
.tokenSource(TokenSource.IDENTITY_PROVIDER)
@@ -139,6 +139,31 @@ private AuthenticationResult createFromManagedIdentityResponse(ManagedIdentityRe
139139
.build();
140140
}
141141

142+
static long getExpiresOnFromManagedIdentityTimestamp(String dateTimeStamp) {
143+
if (dateTimeStamp == null || dateTimeStamp.isEmpty()) {
144+
return 0;
145+
}
146+
147+
// Try parsing as Unix timestamp (seconds since epoch)
148+
try {
149+
return Long.parseLong(dateTimeStamp);
150+
} catch (NumberFormatException e) {
151+
// Not a number
152+
}
153+
154+
// Try parsing as ISO 8601
155+
try {
156+
return Instant.parse(dateTimeStamp).getEpochSecond();
157+
} catch (Exception e) {
158+
// Not ISO 8601
159+
}
160+
161+
throw new MsalClientException(
162+
String.format("Failed to parse timestamp '%s'. Expected Unix epoch seconds or ISO 8601 format.",
163+
dateTimeStamp),
164+
AuthenticationErrorCode.INVALID_TIMESTAMP_FORMAT);
165+
}
166+
142167
private long calculateRefreshOn(long expiresOn) {
143168
long timestampSeconds = System.currentTimeMillis() / 1000;
144169
long expiresIn = expiresOn - timestampSeconds;

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,6 @@ public class AuthenticationErrorCode {
153153
* or performing other cryptographic functions.
154154
*/
155155
public static final String CRYPTO_ERROR = "crypto_error";
156+
157+
public static final String INVALID_TIMESTAMP_FORMAT = "invalid_timestamp_format";
156158
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.aad.msal4j;
5+
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.params.ParameterizedTest;
8+
import org.junit.jupiter.params.provider.ValueSource;
9+
10+
import java.time.Instant;
11+
import java.time.format.DateTimeFormatter;
12+
import java.time.temporal.ChronoUnit;
13+
14+
import static org.junit.jupiter.api.Assertions.*;
15+
16+
class DateTimeTests {
17+
18+
@Test
19+
void parseUnixTimestampInSeconds() {
20+
long currentTimestamp = System.currentTimeMillis() / 1000;
21+
long result = AcquireTokenByManagedIdentitySupplier.getExpiresOnFromManagedIdentityTimestamp(String.valueOf(currentTimestamp));
22+
23+
assertEquals(currentTimestamp, result, "Should parse Unix timestamp in seconds correctly");
24+
}
25+
26+
@Test
27+
void parseIso8601Format() {
28+
// Creates a timestamp in ISO 8601 format, with 24 hours added to it to represent a 24-hour token
29+
String timestamp = DateTimeFormatter.ISO_INSTANT.format(Instant.now().plus(24, ChronoUnit.HOURS));
30+
31+
long expected = Instant.parse(timestamp).getEpochSecond();
32+
long result = AcquireTokenByManagedIdentitySupplier.getExpiresOnFromManagedIdentityTimestamp(timestamp);
33+
34+
assertEquals(expected, result, "Should parse ISO 8601 format correctly");
35+
}
36+
37+
38+
@ParameterizedTest
39+
@ValueSource(strings = {
40+
"2025-05-15T12:34:56Z", // Basic UTC format
41+
"2025-05-15T12:34:56.1234Z", // With milliseconds
42+
"2025-05-15T12:34:56.123456789Z" // With nanoseconds
43+
})
44+
void testValidIso8601Formats(String timestamp) {
45+
long expected = Instant.parse(timestamp).getEpochSecond();
46+
long result = AcquireTokenByManagedIdentitySupplier.getExpiresOnFromManagedIdentityTimestamp(timestamp);
47+
48+
assertEquals(expected, result, "Should parse ISO 8601 format correctly");
49+
}
50+
51+
@Test
52+
void handleNullTimestamp() {
53+
long result = AcquireTokenByManagedIdentitySupplier.getExpiresOnFromManagedIdentityTimestamp(null);
54+
55+
assertEquals(0, result, "Should return 0 for null timestamp");
56+
}
57+
58+
@Test
59+
void handleEmptyTimestamp() {
60+
long result = AcquireTokenByManagedIdentitySupplier.getExpiresOnFromManagedIdentityTimestamp("");
61+
62+
assertEquals(0, result, "Should return 0 for empty timestamp");
63+
}
64+
65+
@Test
66+
void handleInvalidFormat() {
67+
String invalidTimestamp = "not-a-timestamp";
68+
69+
MsalClientException exception = assertThrows(
70+
MsalClientException.class,
71+
() -> AcquireTokenByManagedIdentitySupplier.getExpiresOnFromManagedIdentityTimestamp(invalidTimestamp)
72+
);
73+
74+
assertEquals("invalid_timestamp_format", exception.errorCode());
75+
assertTrue(exception.getMessage().contains(invalidTimestamp),
76+
"Error message should contain the invalid timestamp");
77+
}
78+
}

msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityTests.java

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,21 @@
44
package com.microsoft.aad.msal4j;
55

66
import com.nimbusds.oauth2.sdk.util.URLUtils;
7-
import labapi.App;
87
import org.junit.jupiter.api.Nested;
98
import org.junit.jupiter.api.Test;
109
import org.junit.jupiter.api.TestInstance;
1110
import org.junit.jupiter.api.extension.ExtendWith;
1211
import org.junit.jupiter.params.ParameterizedTest;
1312
import org.junit.jupiter.params.provider.MethodSource;
1413
import org.junit.jupiter.params.provider.ValueSource;
15-
import org.mockito.ArgumentCaptor;
1614
import org.mockito.junit.jupiter.MockitoExtension;
1715

1816
import java.net.SocketException;
1917
import java.nio.file.Path;
2018
import java.nio.file.Paths;
19+
import java.time.Instant;
20+
import java.time.format.DateTimeFormatter;
21+
import java.time.temporal.ChronoUnit;
2122
import java.util.Collections;
2223
import java.util.HashMap;
2324
import java.util.List;
@@ -53,6 +54,12 @@ private String getSuccessfulResponse(String resource) {
5354
"\"Bearer\",\"client_id\":\"client_id\"}";
5455
}
5556

57+
private String getSuccessfulResponseWithISOExpiresOn(String resource) {
58+
String expiresOn = DateTimeFormatter.ISO_INSTANT.format(Instant.now().plus(24, ChronoUnit.HOURS));//A long-lived, 24 hour token
59+
return "{\"access_token\":\"accesstoken\",\"expires_on\":\"" + expiresOn + "\",\"resource\":\"" + resource + "\",\"token_type\":" +
60+
"\"Bearer\",\"client_id\":\"client_id\"}";
61+
}
62+
5663
private String getSuccessfulResponseWithInvalidJson() {
5764
return "missing starting bracket \"access_token\":\"accesstoken\",\"token_type\":" + "\"Bearer\",\"client_id\":\"a bunch of problems}";
5865
}
@@ -313,6 +320,39 @@ void managedIdentityTest_RefreshOnHalfOfExpiresOn() throws Exception {
313320
verify(httpClientMock, times(1)).send(any());
314321
}
315322

323+
@Test
324+
void managedIdentityTest_ISOExpiresOn() throws Exception {
325+
//All managed identity flows use the same AcquireTokenByManagedIdentitySupplier where refreshOn is set,
326+
// so any of the MI options should let us verify that it's being set correctly
327+
IEnvironmentVariables environmentVariables = new EnvironmentVariablesHelper(ManagedIdentitySourceType.APP_SERVICE, appServiceEndpoint);
328+
ManagedIdentityApplication.setEnvironmentVariables(environmentVariables);
329+
DefaultHttpClient httpClientMock = mock(DefaultHttpClient.class);
330+
331+
when(httpClientMock.send(expectedRequest(ManagedIdentitySourceType.APP_SERVICE, resource))).thenReturn(expectedResponse(200, getSuccessfulResponseWithISOExpiresOn(resource)));
332+
333+
miApp = ManagedIdentityApplication
334+
.builder(ManagedIdentityId.systemAssigned())
335+
.httpClient(httpClientMock)
336+
.build();
337+
338+
// Clear caching to avoid cross test pollution.
339+
miApp.tokenCache().accessTokens.clear();
340+
341+
AuthenticationResult result = (AuthenticationResult) miApp.acquireTokenForManagedIdentity(
342+
ManagedIdentityParameters.builder(resource)
343+
.build()).get();
344+
345+
// Calculate what the expected expiration time should be
346+
long expectedExpiresOn = System.currentTimeMillis() / 1000 + (24 * 3600); // 24 hours from now, used in getSuccessfulResponseWithISOExpiresOn
347+
348+
assertNotNull(result.accessToken());
349+
assertEquals(TokenSource.IDENTITY_PROVIDER, result.metadata().tokenSource());
350+
//Allow a few seconds of difference to account for execution time
351+
assertTrue((result.expiresOn() - expectedExpiresOn) <= 5);
352+
353+
verify(httpClientMock, times(1)).send(any());
354+
}
355+
316356
@ParameterizedTest
317357
@MethodSource("com.microsoft.aad.msal4j.ManagedIdentityTestDataProvider#createDataUserAssignedNotSupported")
318358
void managedIdentityTest_UserAssigned_NotSupported(ManagedIdentitySourceType source, String endpoint, ManagedIdentityId id) throws Exception {

0 commit comments

Comments
 (0)