@@ -5,7 +5,11 @@ import { AccessToken, GetTokenOptions, TokenCredential } from "@azure/core-auth"
5
5
6
6
import { IdentityClient } from "../../client/identityClient" ;
7
7
import { TokenCredentialOptions } from "../../tokenCredentialOptions" ;
8
- import { AuthenticationError , CredentialUnavailableError } from "../../errors" ;
8
+ import {
9
+ AuthenticationError ,
10
+ AuthenticationRequiredError ,
11
+ CredentialUnavailableError ,
12
+ } from "../../errors" ;
9
13
import { credentialLogger , formatError , formatSuccess } from "../../util/logging" ;
10
14
import { appServiceMsi2017 } from "./appServiceMsi2017" ;
11
15
import { tracingClient } from "../../util/tracing" ;
@@ -16,6 +20,9 @@ import { arcMsi } from "./arcMsi";
16
20
import { tokenExchangeMsi } from "./tokenExchangeMsi" ;
17
21
import { fabricMsi } from "./fabricMsi" ;
18
22
import { appServiceMsi2019 } from "./appServiceMsi2019" ;
23
+ import { AppTokenProviderParameters , ConfidentialClientApplication } from "@azure/msal-node" ;
24
+ import { DeveloperSignOnClientId } from "../../constants" ;
25
+ import { MsalResult , MsalToken } from "../../msal/types" ;
19
26
20
27
const logger = credentialLogger ( "ManagedIdentityCredential" ) ;
21
28
@@ -59,6 +66,7 @@ export class ManagedIdentityCredential implements TokenCredential {
59
66
private resourceId : string | undefined ;
60
67
private isEndpointUnavailable : boolean | null = null ;
61
68
private isAvailableIdentityClient : IdentityClient ;
69
+ private confidentialApp : ConfidentialClientApplication ;
62
70
63
71
/**
64
72
* Creates an instance of ManagedIdentityCredential with the client ID of a
@@ -113,6 +121,19 @@ export class ManagedIdentityCredential implements TokenCredential {
113
121
maxRetries : 0 ,
114
122
} ,
115
123
} ) ;
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
+ } ) ;
116
137
}
117
138
118
139
private cachedMSI : MSI | undefined ;
@@ -167,7 +188,6 @@ export class ManagedIdentityCredential implements TokenCredential {
167
188
try {
168
189
// Determining the available MSI, and avoiding checking for other MSIs while the program is running.
169
190
const availableMSI = await this . cachedAvailableMSI ( scopes , updatedOptions ) ;
170
-
171
191
return availableMSI . getToken (
172
192
{
173
193
identityClient : this . identityClient ,
@@ -202,19 +222,56 @@ export class ManagedIdentityCredential implements TokenCredential {
202
222
options ?: GetTokenOptions
203
223
) : Promise < AccessToken > {
204
224
let result : AccessToken | null = null ;
205
-
206
225
const { span, updatedOptions } = tracingClient . startSpan (
207
226
`${ ManagedIdentityCredential . name } .getToken` ,
208
227
options
209
228
) ;
210
-
211
229
try {
212
230
// isEndpointAvailable can be true, false, or null,
213
231
// If it's null, it means we don't yet know whether
214
232
// the endpoint is available and need to check for it.
215
233
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
+ } ) ;
217
252
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 ) ;
218
275
if ( result === null ) {
219
276
// If authenticateManagedIdentity returns null,
220
277
// it means no MSI endpoints are available.
@@ -312,4 +369,50 @@ export class ManagedIdentityCredential implements TokenCredential {
312
369
span . end ( ) ;
313
370
}
314
371
}
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
+ }
315
418
}
0 commit comments