Skip to content

Managed Identity - Version 2 #7587

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion lib/msal-node/src/client/ManagedIdentityClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -116,6 +118,8 @@ export class ManagedIdentityClient {
AzureArc.getEnvironmentVariables()
)
? ManagedIdentitySourceNames.AZURE_ARC
: ImdsV2.isCredentialEndpointAvailable()
? ManagedIdentitySourceNames.IMDSV2
: ManagedIdentitySourceNames.DEFAULT_TO_IMDS;

return ManagedIdentityClient.sourceName;
Expand Down Expand Up @@ -172,6 +176,12 @@ export class ManagedIdentityClient {
disableInternalRetries,
managedIdentityId
) ||
ImdsV2.tryCreate(
logger,
nodeStorage,
networkClient,
cryptoProvider
) ||
Imds.tryCreate(
logger,
nodeStorage,
Expand Down
81 changes: 43 additions & 38 deletions lib/msal-node/src/client/ManagedIdentitySources/Imds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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}`;
}
};
}
171 changes: 171 additions & 0 deletions lib/msal-node/src/client/ManagedIdentitySources/ImdsV2.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
16 changes: 16 additions & 0 deletions lib/msal-node/src/response/ShortLivedCredentialResponse.ts
Original file line number Diff line number Diff line change
@@ -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;
};
2 changes: 2 additions & 0 deletions lib/msal-node/src/utils/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down
Loading