Skip to content

Commit 45528b9

Browse files
[Identity] Add Managed Identity token caching support (Azure#22862)
1 parent c448098 commit 45528b9

File tree

6 files changed

+179
-31
lines changed

6 files changed

+179
-31
lines changed

sdk/identity/identity/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Release History
22

3+
# 3.1.0-beta.1 (Unreleased)
4+
5+
### Features Added
6+
7+
- Added Token Caching support to Managed Identity Credential
8+
### Breaking Changes
9+
10+
### Bugs Fixed
11+
12+
### Other Changes
13+
314
## 3.0.0 (2022-09-19)
415

516
### Features Added

sdk/identity/identity/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@azure/identity",
33
"sdk-type": "client",
4-
"version": "3.0.0",
4+
"version": "3.1.0-beta.1",
55
"description": "Provides credential implementations for Azure SDK libraries that can authenticate with Azure Active Directory",
66
"main": "dist/index.js",
77
"module": "dist-esm/src/index.js",

sdk/identity/identity/src/client/identityClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import { tracingClient } from "../util/tracing";
1919
import { logger } from "../util/logging";
2020
import { TokenCredentialOptions } from "../tokenCredentialOptions";
2121
import {
22-
parseExpiresOn,
2322
TokenResponseParsedBody,
23+
parseExpiresOn,
2424
} from "../credentials/managedIdentityCredential/utils";
2525

2626
const noCorrelationId = "noCorrelationId";

sdk/identity/identity/src/credentials/managedIdentityCredential/index.ts

Lines changed: 108 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { AccessToken, GetTokenOptions, TokenCredential } from "@azure/core-auth"
55

66
import { IdentityClient } from "../../client/identityClient";
77
import { TokenCredentialOptions } from "../../tokenCredentialOptions";
8-
import { AuthenticationError, CredentialUnavailableError } from "../../errors";
8+
import {
9+
AuthenticationError,
10+
AuthenticationRequiredError,
11+
CredentialUnavailableError,
12+
} from "../../errors";
913
import { credentialLogger, formatError, formatSuccess } from "../../util/logging";
1014
import { appServiceMsi2017 } from "./appServiceMsi2017";
1115
import { tracingClient } from "../../util/tracing";
@@ -16,6 +20,9 @@ import { arcMsi } from "./arcMsi";
1620
import { tokenExchangeMsi } from "./tokenExchangeMsi";
1721
import { fabricMsi } from "./fabricMsi";
1822
import { appServiceMsi2019 } from "./appServiceMsi2019";
23+
import { AppTokenProviderParameters, ConfidentialClientApplication } from "@azure/msal-node";
24+
import { DeveloperSignOnClientId } from "../../constants";
25+
import { MsalResult, MsalToken } from "../../msal/types";
1926

2027
const logger = credentialLogger("ManagedIdentityCredential");
2128

@@ -59,6 +66,7 @@ export class ManagedIdentityCredential implements TokenCredential {
5966
private resourceId: string | undefined;
6067
private isEndpointUnavailable: boolean | null = null;
6168
private isAvailableIdentityClient: IdentityClient;
69+
private confidentialApp: ConfidentialClientApplication;
6270

6371
/**
6472
* Creates an instance of ManagedIdentityCredential with the client ID of a
@@ -113,6 +121,19 @@ export class ManagedIdentityCredential implements TokenCredential {
113121
maxRetries: 0,
114122
},
115123
});
124+
/** authority host validation and metadata discovery to be skipped in managed identity
125+
* since this wasn't done previously before adding token cache support
126+
*/
127+
this.confidentialApp = new ConfidentialClientApplication({
128+
auth: {
129+
clientId: this.clientId ?? DeveloperSignOnClientId,
130+
clientSecret: "dummy-secret",
131+
cloudDiscoveryMetadata:
132+
'{"tenant_discovery_endpoint":"https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration","api-version":"1.1","metadata":[{"preferred_network":"login.microsoftonline.com","preferred_cache":"login.windows.net","aliases":["login.microsoftonline.com","login.windows.net","login.microsoft.com","sts.windows.net"]},{"preferred_network":"login.partner.microsoftonline.cn","preferred_cache":"login.partner.microsoftonline.cn","aliases":["login.partner.microsoftonline.cn","login.chinacloudapi.cn"]},{"preferred_network":"login.microsoftonline.de","preferred_cache":"login.microsoftonline.de","aliases":["login.microsoftonline.de"]},{"preferred_network":"login.microsoftonline.us","preferred_cache":"login.microsoftonline.us","aliases":["login.microsoftonline.us","login.usgovcloudapi.net"]},{"preferred_network":"login-us.microsoftonline.com","preferred_cache":"login-us.microsoftonline.com","aliases":["login-us.microsoftonline.com"]}]}',
133+
authorityMetadata:
134+
'{"token_endpoint":"https://login.microsoftonline.com/common/oauth2/v2.0/token","token_endpoint_auth_methods_supported":["client_secret_post","private_key_jwt","client_secret_basic"],"jwks_uri":"https://login.microsoftonline.com/common/discovery/v2.0/keys","response_modes_supported":["query","fragment","form_post"],"subject_types_supported":["pairwise"],"id_token_signing_alg_values_supported":["RS256"],"response_types_supported":["code","id_token","code id_token","id_token token"],"scopes_supported":["openid","profile","email","offline_access"],"issuer":"https://login.microsoftonline.com/{tenantid}/v2.0","request_uri_parameter_supported":false,"userinfo_endpoint":"https://graph.microsoft.com/oidc/userinfo","authorization_endpoint":"https://login.microsoftonline.com/common/oauth2/v2.0/authorize","device_authorization_endpoint":"https://login.microsoftonline.com/common/oauth2/v2.0/devicecode","http_logout_supported":true,"frontchannel_logout_supported":true,"end_session_endpoint":"https://login.microsoftonline.com/common/oauth2/v2.0/logout","claims_supported":["sub","iss","cloud_instance_name","cloud_instance_host_name","cloud_graph_host_name","msgraph_host","aud","exp","iat","auth_time","acr","nonce","preferred_username","name","tid","ver","at_hash","c_hash","email"],"kerberos_endpoint":"https://login.microsoftonline.com/common/kerberos","tenant_region_scope":null,"cloud_instance_name":"microsoftonline.com","cloud_graph_host_name":"graph.windows.net","msgraph_host":"graph.microsoft.com","rbac_url":"https://pas.windows.net"}',
135+
},
136+
});
116137
}
117138

118139
private cachedMSI: MSI | undefined;
@@ -167,7 +188,6 @@ export class ManagedIdentityCredential implements TokenCredential {
167188
try {
168189
// Determining the available MSI, and avoiding checking for other MSIs while the program is running.
169190
const availableMSI = await this.cachedAvailableMSI(scopes, updatedOptions);
170-
171191
return availableMSI.getToken(
172192
{
173193
identityClient: this.identityClient,
@@ -202,19 +222,56 @@ export class ManagedIdentityCredential implements TokenCredential {
202222
options?: GetTokenOptions
203223
): Promise<AccessToken> {
204224
let result: AccessToken | null = null;
205-
206225
const { span, updatedOptions } = tracingClient.startSpan(
207226
`${ManagedIdentityCredential.name}.getToken`,
208227
options
209228
);
210-
211229
try {
212230
// isEndpointAvailable can be true, false, or null,
213231
// If it's null, it means we don't yet know whether
214232
// the endpoint is available and need to check for it.
215233
if (this.isEndpointUnavailable !== true) {
216-
result = await this.authenticateManagedIdentity(scopes, updatedOptions);
234+
const appTokenParameters: AppTokenProviderParameters = {
235+
correlationId: this.identityClient.getCorrelationId(),
236+
tenantId: options?.tenantId || "organizations",
237+
scopes: [...scopes],
238+
claims: options?.claims,
239+
};
240+
241+
this.confidentialApp.SetAppTokenProvider(
242+
async (appTokenProviderParameters = appTokenParameters) => {
243+
logger.info(
244+
`SetAppTokenProvider invoked with parameters- ${JSON.stringify(
245+
appTokenProviderParameters
246+
)}`
247+
);
248+
const resultToken = await this.authenticateManagedIdentity(scopes, {
249+
...updatedOptions,
250+
...appTokenProviderParameters,
251+
});
217252

253+
if (resultToken) {
254+
logger.info(`SetAppTokenProvider has saved the token in cache`);
255+
logger.info(`token = ${resultToken.token}`);
256+
return {
257+
accessToken: resultToken?.token,
258+
expiresInSeconds: resultToken?.expiresOnTimestamp,
259+
};
260+
} else {
261+
logger.info(
262+
`SetAppTokenProvider token has "no_access_token_returned" as the saved token`
263+
);
264+
return {
265+
accessToken: "no_access_token_returned",
266+
expiresInSeconds: 0,
267+
};
268+
}
269+
}
270+
);
271+
const authenticationResult = await this.confidentialApp.acquireTokenByClientCredential({
272+
...appTokenParameters,
273+
});
274+
result = this.handleResult(scopes, authenticationResult || undefined);
218275
if (result === null) {
219276
// If authenticateManagedIdentity returns null,
220277
// it means no MSI endpoints are available.
@@ -312,4 +369,50 @@ export class ManagedIdentityCredential implements TokenCredential {
312369
span.end();
313370
}
314371
}
372+
373+
/**
374+
* Handles the MSAL authentication result.
375+
* If the result has an account, we update the local account reference.
376+
* If the token received is invalid, an error will be thrown depending on what's missing.
377+
*/
378+
private handleResult(
379+
scopes: string | string[],
380+
result?: MsalResult,
381+
getTokenOptions?: GetTokenOptions
382+
): AccessToken {
383+
this.ensureValidMsalToken(scopes, result, getTokenOptions);
384+
logger.getToken.info(formatSuccess(scopes));
385+
return {
386+
token: result!.accessToken!,
387+
expiresOnTimestamp: result!.expiresOn!.getTime(),
388+
};
389+
}
390+
391+
/**
392+
* Ensures the validity of the MSAL token
393+
* @internal
394+
*/
395+
private ensureValidMsalToken(
396+
scopes: string | string[],
397+
msalToken?: MsalToken,
398+
getTokenOptions?: GetTokenOptions
399+
): void {
400+
const error = (message: string): Error => {
401+
logger.getToken.info(message);
402+
return new AuthenticationRequiredError({
403+
scopes: Array.isArray(scopes) ? scopes : [scopes],
404+
getTokenOptions,
405+
message,
406+
});
407+
};
408+
if (!msalToken) {
409+
throw error("No response");
410+
}
411+
if (!msalToken.expiresOn) {
412+
throw error(`Response had no "expiresOn" property.`);
413+
}
414+
if (!msalToken.accessToken) {
415+
throw error(`Response had no "accessToken" property.`);
416+
}
417+
}
315418
}

sdk/identity/identity/test/internal/node/azureApplicationCredential.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ describe("AzureApplicationCredential testing Managed Identity (internal)", funct
114114
"URL does not have expected version"
115115
);
116116
if (authDetails.result?.token) {
117-
assert.equal(authDetails.result.expiresOnTimestamp, 1560999478000);
117+
assert.equal(authDetails.result.expiresOnTimestamp, 1560999478000000);
118118
} else {
119119
assert.fail("No token was returned!");
120120
}

0 commit comments

Comments
 (0)