diff --git a/lib/msal-node/src/client/ManagedIdentityClient.ts b/lib/msal-node/src/client/ManagedIdentityClient.ts index 798606736a..83708cf92c 100644 --- a/lib/msal-node/src/client/ManagedIdentityClient.ts +++ b/lib/msal-node/src/client/ManagedIdentityClient.ts @@ -25,6 +25,7 @@ import { NodeStorage } from "../cache/NodeStorage.js"; import { BaseManagedIdentitySource } from "./ManagedIdentitySources/BaseManagedIdentitySource.js"; import { ManagedIdentitySourceNames } from "../utils/Constants.js"; import { MachineLearning } from "./ManagedIdentitySources/MachineLearning.js"; +import { ImdsV2 } from "./ManagedIdentitySources/ImdsV2.js"; /* * Class to initialize a managed identity and identify the service. @@ -91,7 +92,8 @@ export class ManagedIdentityClient { } /** - * Determine the Managed Identity Source based on available environment variables. This API is consumed by ManagedIdentityApplication's getManagedIdentitySource. + * Determine the Managed Identity Source based on available environment variables and probing an IMDS credential endpoint. + * This API is consumed by ManagedIdentityApplication's getManagedIdentitySource. * @returns ManagedIdentitySourceNames - The Managed Identity source's name */ public getManagedIdentitySource(): ManagedIdentitySourceNames { @@ -116,6 +118,8 @@ export class ManagedIdentityClient { AzureArc.getEnvironmentVariables() ) ? ManagedIdentitySourceNames.AZURE_ARC + : ImdsV2.isCredentialEndpointAvailable() + ? ManagedIdentitySourceNames.IMDSV2 : ManagedIdentitySourceNames.DEFAULT_TO_IMDS; return ManagedIdentityClient.sourceName; @@ -172,6 +176,12 @@ export class ManagedIdentityClient { disableInternalRetries, managedIdentityId ) || + ImdsV2.tryCreate( + logger, + nodeStorage, + networkClient, + cryptoProvider + ) || Imds.tryCreate( logger, nodeStorage, diff --git a/lib/msal-node/src/client/ManagedIdentitySources/Imds.ts b/lib/msal-node/src/client/ManagedIdentitySources/Imds.ts index fd97abceed..f51743439b 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/Imds.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/Imds.ts @@ -22,9 +22,7 @@ import { ImdsRetryPolicy } from "../../retry/ImdsRetryPolicy.js"; // IMDS constants. Docs for IMDS are available here https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http const IMDS_TOKEN_PATH: string = "/metadata/identity/oauth2/token"; -const DEFAULT_IMDS_ENDPOINT: string = `http://169.254.169.254${IMDS_TOKEN_PATH}`; - -const IMDS_API_VERSION: string = "2018-02-01"; +export const IMDS_API_VERSION: string = "2018-02-01"; // Original source of code: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/src/ImdsManagedIdentitySource.cs export class Imds extends BaseManagedIdentitySource { @@ -56,41 +54,8 @@ export class Imds extends BaseManagedIdentitySource { cryptoProvider: CryptoProvider, disableInternalRetries: boolean ): Imds { - let validatedIdentityEndpoint: string; - - if ( - process.env[ - ManagedIdentityEnvironmentVariableNames - .AZURE_POD_IDENTITY_AUTHORITY_HOST - ] - ) { - logger.info( - `[Managed Identity] Environment variable ${ - ManagedIdentityEnvironmentVariableNames.AZURE_POD_IDENTITY_AUTHORITY_HOST - } for ${ManagedIdentitySourceNames.IMDS} returned endpoint: ${ - process.env[ - ManagedIdentityEnvironmentVariableNames - .AZURE_POD_IDENTITY_AUTHORITY_HOST - ] - }` - ); - validatedIdentityEndpoint = Imds.getValidatedEnvVariableUrlString( - ManagedIdentityEnvironmentVariableNames.AZURE_POD_IDENTITY_AUTHORITY_HOST, - `${ - process.env[ - ManagedIdentityEnvironmentVariableNames - .AZURE_POD_IDENTITY_AUTHORITY_HOST - ] - }${IMDS_TOKEN_PATH}`, - ManagedIdentitySourceNames.IMDS, - logger - ); - } else { - logger.info( - `[Managed Identity] Unable to find ${ManagedIdentityEnvironmentVariableNames.AZURE_POD_IDENTITY_AUTHORITY_HOST} environment variable for ${ManagedIdentitySourceNames.IMDS}, using the default endpoint.` - ); - validatedIdentityEndpoint = DEFAULT_IMDS_ENDPOINT; - } + const validatedIdentityEndpoint: string = + this.getValidatedIdentityEndpoint(IMDS_TOKEN_PATH, logger); return new Imds( logger, @@ -136,4 +101,44 @@ export class Imds extends BaseManagedIdentitySource { return request; } + + public static getValidatedIdentityEndpoint = ( + subPath: string, + logger: Logger + ): string => { + if ( + process.env[ + ManagedIdentityEnvironmentVariableNames + .AZURE_POD_IDENTITY_AUTHORITY_HOST + ] + ) { + logger.info( + `[Managed Identity] Environment variable ${ + ManagedIdentityEnvironmentVariableNames.AZURE_POD_IDENTITY_AUTHORITY_HOST + } for ${ManagedIdentitySourceNames.IMDS} returned endpoint: ${ + process.env[ + ManagedIdentityEnvironmentVariableNames + .AZURE_POD_IDENTITY_AUTHORITY_HOST + ] + }` + ); + + return Imds.getValidatedEnvVariableUrlString( + ManagedIdentityEnvironmentVariableNames.AZURE_POD_IDENTITY_AUTHORITY_HOST, + `${ + process.env[ + ManagedIdentityEnvironmentVariableNames + .AZURE_POD_IDENTITY_AUTHORITY_HOST + ] + }${subPath}`, + ManagedIdentitySourceNames.IMDS, + logger + ); + } else { + logger.info( + `[Managed Identity] Unable to find ${ManagedIdentityEnvironmentVariableNames.AZURE_POD_IDENTITY_AUTHORITY_HOST} environment variable for ${ManagedIdentitySourceNames.IMDS}, using the default endpoint.` + ); + return `http://169.254.169.254${subPath}`; + } + }; } diff --git a/lib/msal-node/src/client/ManagedIdentitySources/ImdsV2.ts b/lib/msal-node/src/client/ManagedIdentitySources/ImdsV2.ts new file mode 100644 index 0000000000..c121c156e0 --- /dev/null +++ b/lib/msal-node/src/client/ManagedIdentitySources/ImdsV2.ts @@ -0,0 +1,171 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { INetworkModule, Logger } from "@azure/msal-common/node"; +// import { Agent } from "https"; +import { ManagedIdentityId } from "../../config/ManagedIdentityId.js"; +import { ManagedIdentityRequestParameters } from "../../config/ManagedIdentityRequestParameters.js"; +import { BaseManagedIdentitySource } from "./BaseManagedIdentitySource.js"; +import { CryptoProvider } from "../../crypto/CryptoProvider.js"; +import { + API_VERSION_QUERY_PARAMETER_NAME, + CLIENT_REQUEST_ID_HEADER_NAME, + HttpMethod, + METADATA_HEADER_NAME, + ManagedIdentityIdType, + RESOURCE_BODY_OR_QUERY_PARAMETER_NAME, +} from "../../utils/Constants.js"; +import { NodeStorage } from "../../cache/NodeStorage.js"; +import { Imds, IMDS_API_VERSION } from "./Imds.js"; +import { ShortLivedCredential } from "../../response/ShortLivedCredentialResponse.js"; + +const CREDENTIAL_PATH: string = + "/metadata/identity/credential?cred-api-version=1.0"; + +export class ImdsV2 extends BaseManagedIdentitySource { + private identityEndpoint: string; + + constructor( + logger: Logger, + nodeStorage: NodeStorage, + networkClient: INetworkModule, + cryptoProvider: CryptoProvider, + identityEndpoint: string + ) { + super(logger, nodeStorage, networkClient, cryptoProvider); + + this.identityEndpoint = identityEndpoint; + } + + public static tryCreate( + logger: Logger, + nodeStorage: NodeStorage, + networkClient: INetworkModule, + cryptoProvider: CryptoProvider + ): ImdsV2 | null { + if (!this.isCredentialEndpointAvailable()) { + return null; + } + + const validatedIdentityEndpoint: string = + Imds.getValidatedIdentityEndpoint(CREDENTIAL_PATH, logger); + + return new ImdsV2( + logger, + nodeStorage, + networkClient, + cryptoProvider, + validatedIdentityEndpoint + ); + } + + public static isCredentialEndpointAvailable(): boolean { + // TODO: Probe credential endpoint. If it doesn't return 200, return null + return true; + } + + public createRequest( + resource: string, + managedIdentityId: ManagedIdentityId + ): ManagedIdentityRequestParameters { + const imdsRequest: ManagedIdentityRequestParameters = + new ManagedIdentityRequestParameters( + HttpMethod.POST, + this.identityEndpoint + ); + + imdsRequest.headers[METADATA_HEADER_NAME] = "true"; + imdsRequest.headers[CLIENT_REQUEST_ID_HEADER_NAME] = "1234567890"; // TODO: generate random request ID + + imdsRequest.queryParameters[API_VERSION_QUERY_PARAMETER_NAME] = + IMDS_API_VERSION; + imdsRequest.queryParameters[RESOURCE_BODY_OR_QUERY_PARAMETER_NAME] = + resource; + + if ( + managedIdentityId.idType !== ManagedIdentityIdType.SYSTEM_ASSIGNED + ) { + imdsRequest.queryParameters[ + this.getManagedIdentityUserAssignedIdQueryParameterKey( + managedIdentityId.idType, + true // indicates source is IMDS + ) + ] = managedIdentityId.id; + } + + /* + * TODO: add self-signed mTLS certificate functionality + * If Windows, check certificate store for mTLS certificate (no Linux support) + * Otherwise, check in-memory cache for mTLS certificate + * If not either of the above, create self-signed mTLS certificate + */ + /* + * const mTLSCertificatePem: string = "fake_cert"; + * const privateKeyPem: string = "fake_private_key"; + */ + const sha256HashOfPublicKey: string = "fake_sha256_hash_of_public_key"; + const x5C: string = "fake_x5c"; + imdsRequest.bodyParameters = { + cnf: JSON.stringify({ + jwk: { + kty: "RSA", + use: "sig", + alg: "RS256", + kid: sha256HashOfPublicKey, + x5c: [x5C], + }, + }), + latch_key: "false", + }; + + /* + * TODO: Request SLC via "/credential" endpoint instead of using this fake object. + * This will be complicated the current acquireTokenWithManagedIdentity function in + * BaseManagedIdentitySource is not built to handle this request. + */ + const shortLivedCredential: ShortLivedCredential = { + client_id: "fake_string", + credential: "fake_string", + expires_in: 3599, + identity_type: "fake_string", + refresh_in: 3599, + region: "fake_string", + regional_token_url: "fake_string", + tenant_id: "fake_string", + }; + + const estsRequest: ManagedIdentityRequestParameters = + new ManagedIdentityRequestParameters( + HttpMethod.POST, + `${shortLivedCredential.regional_token_url}/${shortLivedCredential.tenant_id}/oauth2/v2.0/token` + ); + + // TODO: define constants for these values + estsRequest.bodyParameters = { + grant_type: "client_credentials", + scope: "https://management.azure.com/.default", + client_id: shortLivedCredential.client_id, + client_assertion: shortLivedCredential.credential, + client_assertion_type: + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + }; + + /* + * TODO: + * 1. Re-work the HttpClient to handle the self-signed mTLS certificate + * 2. Add functionality to ManagedIdentityRequestParameters to handle the self-signed mTLS certificate + */ + /* + * const agent = new Agent({ + * cert: mTLSCertificatePem, + * key: privateKeyPem, + * ca: mTLSCertificatePem, + * }); + * estsRequest.agent = agent; + */ + + return estsRequest; + } +} diff --git a/lib/msal-node/src/response/ShortLivedCredentialResponse.ts b/lib/msal-node/src/response/ShortLivedCredentialResponse.ts new file mode 100644 index 0000000000..d077f357c6 --- /dev/null +++ b/lib/msal-node/src/response/ShortLivedCredentialResponse.ts @@ -0,0 +1,16 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +// TODO: Add documentation +export type ShortLivedCredential = { + client_id: string; + credential: string; + expires_in: number; + identity_type: string; + refresh_in: number; + region: string; + regional_token_url: string; + tenant_id: string; +}; diff --git a/lib/msal-node/src/utils/Constants.ts b/lib/msal-node/src/utils/Constants.ts index a094e561bc..6b72f776c0 100644 --- a/lib/msal-node/src/utils/Constants.ts +++ b/lib/msal-node/src/utils/Constants.ts @@ -17,6 +17,7 @@ export const RESOURCE_BODY_OR_QUERY_PARAMETER_NAME: string = "resource"; export const DEFAULT_MANAGED_IDENTITY_ID = "system_assigned_managed_identity"; export const MANAGED_IDENTITY_DEFAULT_TENANT = "managed_identity"; export const DEFAULT_AUTHORITY_FOR_MANAGED_IDENTITY = `https://login.microsoftonline.com/${MANAGED_IDENTITY_DEFAULT_TENANT}/`; +export const CLIENT_REQUEST_ID_HEADER_NAME: string = "X-ms-Client-Request-id"; /** * Managed Identity Environment Variable Names @@ -43,6 +44,7 @@ export const ManagedIdentitySourceNames = { CLOUD_SHELL: "CloudShell", DEFAULT_TO_IMDS: "DefaultToImds", IMDS: "Imds", + IMDSV2: "ImdsV2", MACHINE_LEARNING: "MachineLearning", SERVICE_FABRIC: "ServiceFabric", } as const;