Skip to content

Commit

Permalink
ManagedIdentityTokenResponse's expires_in can now be accepted in ISO …
Browse files Browse the repository at this point in the history
…8601 date format (#7544)

Fixes #7393
  • Loading branch information
Robbie-Microsoft authored Feb 5, 2025
1 parent 4d21e80 commit aac9d4c
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "ManagedIdentityTokenResponse's expires_in can now be accepted in ISO 8601 date format #7544",
"packageName": "@azure/msal-node",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
ManagedIdentityErrorCodes,
createManagedIdentityError,
} from "../../error/ManagedIdentityError.js";
import { isIso8601 } from "../../utils/TimeUtils.js";

/**
* Managed Identity User Assigned Id Query Parameter Names
Expand Down Expand Up @@ -84,6 +85,12 @@ export abstract class BaseManagedIdentitySource {
): ServerAuthorizationTokenResponse {
let refreshIn, expiresIn: number | undefined;
if (response.body.expires_on) {
// if the expires_on field in the response body is a string and in ISO 8601 format, convert it to a Unix timestamp (seconds since epoch)
if (isIso8601(response.body.expires_on)) {
response.body.expires_on =
new Date(response.body.expires_on).getTime() / 1000;
}

expiresIn = response.body.expires_on - TimeUtils.nowSeconds();

// compute refresh_in as 1/2 of expires_in, but only if expires_in > 2h
Expand Down
20 changes: 20 additions & 0 deletions lib/msal-node/src/utils/TimeUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

/**
* @internal
* Checks if a given date string is in ISO 8601 format.
*
* @param dateString - The date string to be checked.
* @returns boolean - Returns true if the date string is in ISO 8601 format, otherwise false.
*/
export function isIso8601(dateString: number | string): boolean {
if (typeof dateString !== "string") {
return false;
}

const date = new Date(dateString);
return !isNaN(date.getTime()) && date.toISOString() === dateString;
}
22 changes: 22 additions & 0 deletions lib/msal-node/test/client/ManagedIdentitySources/Imds.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
MANAGED_IDENTITY_RESOURCE_ID_2,
MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE,
TEST_CONFIG,
TEST_TOKEN_LIFETIMES,
THREE_SECONDS_IN_MILLI,
getCacheKey,
} from "../../test_kit/StringConstants.js";
Expand Down Expand Up @@ -681,6 +682,27 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => {
).toBe(false);
}, 10000); // double the timeout value for this test because it waits two seconds in between the acquireToken call and the cache lookup

test("ensures an ISO 8601 date returned by the Managed Identity is converted to a Unix timestamp (seconds since epoch)", async () => {
// get an ISO 8601 date 3 hours in the future
// (the default length of time in ManagedIdentityNetworkClient's getSuccessResponse())
const threeHoursInMilliseconds =
TEST_TOKEN_LIFETIMES.DEFAULT_EXPIRES_IN * 3 * 1000;
const now = new Date();
now.setTime(now.getTime() + threeHoursInMilliseconds);
const isoDate = now.toISOString();

jest.spyOn(
networkClient,
<any>"sendGetRequestAsync"
).mockReturnValue(networkClient.getSuccessResponse(isoDate));

const { expiresOn } =
await systemAssignedManagedIdentityApplication.acquireToken(
managedIdentityRequestParams
);
expect(expiresOn?.toISOString() === isoDate).toBe(true);
});

test("requests three tokens with two different resources while switching between user and system assigned, then requests them again to verify they are retrieved from the cache, then verifies that their cache keys are correct", async () => {
// the imported systemAssignedManagedIdentityApplication is the default System Assigned Managed Identity Application.
// for reference, in this case it is equivalent to systemAssignedManagedIdentityApplicationResource1
Expand Down
55 changes: 25 additions & 30 deletions lib/msal-node/test/test_kit/ManagedIdentityTestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,27 @@ const EMPTY_HEADERS: Record<string, string> = {};

export class ManagedIdentityNetworkClient implements INetworkModule {
private clientId: string;
private resource: string | undefined;

constructor(clientId: string) {
this.clientId = clientId;
}

// App Service, Azure Arc, Imds, Service Fabric
sendGetRequestAsync<T>(): Promise<NetworkResponse<T>> {
/**
* Generates a successful response body for managed identity token requests.
* @param iso8601Date - Optional ISO 8601 date string for token expiration.
* @returns A ManagedIdentityTokenResponse object containing the access token and other details.
*/
getSuccessResponse<T>(iso8601Date?: string): Promise<NetworkResponse<T>> {
return new Promise<NetworkResponse<T>>((resolve, _reject) => {
resolve({
status: HttpStatus.SUCCESS,
body: {
access_token: TEST_TOKENS.ACCESS_TOKEN,
client_id: this.clientId,
expires_on:
iso8601Date ||
TimeUtils.nowSeconds() +
TEST_TOKEN_LIFETIMES.DEFAULT_EXPIRES_IN * 3, // 3 hours in the future
TEST_TOKEN_LIFETIMES.DEFAULT_EXPIRES_IN * 3, // 3 hours in the future
resource: MANAGED_IDENTITY_RESOURCE.replace(
"/.default",
""
Expand All @@ -53,25 +57,14 @@ export class ManagedIdentityNetworkClient implements INetworkModule {
});
}

// App Service, Azure Arc, Imds, Service Fabric
sendGetRequestAsync<T>(): Promise<NetworkResponse<T>> {
return this.getSuccessResponse();
}

// Cloud Shell
sendPostRequestAsync<T>(): Promise<NetworkResponse<T>> {
return new Promise<NetworkResponse<T>>((resolve, _reject) => {
resolve({
status: HttpStatus.SUCCESS,
body: {
access_token: TEST_TOKENS.ACCESS_TOKEN,
client_id: this.clientId,
expires_on:
TimeUtils.nowSeconds() +
TEST_TOKEN_LIFETIMES.DEFAULT_EXPIRES_IN * 3, // 3 hours in the future
resource: (
this.resource || MANAGED_IDENTITY_RESOURCE
).replace("/.default", ""),
token_type: AuthenticationScheme.BEARER,
} as ManagedIdentityTokenResponse,
headers: EMPTY_HEADERS,
} as NetworkResponse<T>);
});
return this.getSuccessResponse();
}
}

Expand All @@ -92,8 +85,11 @@ export class ManagedIdentityNetworkErrorClient implements INetworkModule {
this.status = status || HttpStatus.SERVER_ERROR;
}

// App Service, Azure Arc, Imds, Service Fabric
sendGetRequestAsync<T>(): Promise<NetworkResponse<T>> {
/**
* Generates an error response body for managed identity token requests.
* @returns A NetworkResponse object containing the error details.
*/
getErrorResponse<T>(): Promise<NetworkResponse<T>> {
return new Promise<NetworkResponse<T>>((resolve, _reject) => {
resolve({
status: this.status,
Expand All @@ -103,15 +99,14 @@ export class ManagedIdentityNetworkErrorClient implements INetworkModule {
});
}

// App Service, Azure Arc, Imds, Service Fabric
sendGetRequestAsync<T>(): Promise<NetworkResponse<T>> {
return this.getErrorResponse();
}

// Cloud Shell
sendPostRequestAsync<T>(): Promise<NetworkResponse<T>> {
return new Promise<NetworkResponse<T>>((resolve, _reject) => {
resolve({
status: this.status,
body: this.errorResponse,
headers: EMPTY_HEADERS,
} as NetworkResponse<T>);
});
return this.getErrorResponse();
}
}

Expand Down

0 comments on commit aac9d4c

Please sign in to comment.