diff --git a/eng/pipelines/templates/jobs/archetype-sdk-client.yml b/eng/pipelines/templates/jobs/archetype-sdk-client.yml index 12aa0e9b1dbb..4ca0dc032131 100644 --- a/eng/pipelines/templates/jobs/archetype-sdk-client.yml +++ b/eng/pipelines/templates/jobs/archetype-sdk-client.yml @@ -20,11 +20,6 @@ parameters: - name: Matrix type: object default: - Linux_Node8: - Pool: $(LinuxPool) - OSVmImage: - NodeTestVersion: "8.x" - TestType: "node" Windows_Node10: Pool: $(WindowsPool) OSVmImage: diff --git a/sdk/identity/identity/CHANGELOG.md b/sdk/identity/identity/CHANGELOG.md index 248ee36d1a92..4db90fcef4bb 100644 --- a/sdk/identity/identity/CHANGELOG.md +++ b/sdk/identity/identity/CHANGELOG.md @@ -1,7 +1,9 @@ # Release History -## 1.5.1 (Unreleased) +## 1.5.1 (2021-08-12) +- Fixed how we verify the IMDS endpoint is available. Now, besides skipping the `Metadata` header, we skip the URL query. Both will ensure that all the known IMDS endpoints return as early as possible. +- Added support for the `AZURE_POD_IDENTITY_AUTHORITY_HOST` environment variable. If present, the IMDS endpoint initial verification will be skipped. ## 1.5.0 (2021-07-19) diff --git a/sdk/identity/identity/README.md b/sdk/identity/identity/README.md index 2f9292b039e2..fbb27ce25076 100644 --- a/sdk/identity/identity/README.md +++ b/sdk/identity/identity/README.md @@ -11,8 +11,10 @@ You can find examples for these various credentials in [Azure Identity Examples ### Currently supported environments - [LTS versions of Node.js](https://nodejs.org/about/releases/) + - **Note:** If your application runs on Node.js v8 or lower and you cannot upgrade your Node.js version to latest stable version, then pin your `@azure/identity` dependency to version 1.1.0. - Latest versions of Safari, Chrome, Edge, and Firefox. - Note: Among the different credentials exported in this library, `InteractiveBrowserCredential` is the only one that is supported in the browser. +- See our [support policy](https://github.com/Azure/azure-sdk-for-js/blob/main/SUPPORT.md) for more details. ### Install the package @@ -24,7 +26,6 @@ npm install --save @azure/identity ### Prerequisites -- Node.js 8 LTS or higher. - An [Azure subscription](https://azure.microsoft.com/free/). - The [Azure CLI][azure_cli] can also be useful for authenticating in a development environment and managing account roles. diff --git a/sdk/identity/identity/src/credentials/managedIdentityCredential/constants.ts b/sdk/identity/identity/src/credentials/managedIdentityCredential/constants.ts index 52026f204d87..430ef07482f1 100644 --- a/sdk/identity/identity/src/credentials/managedIdentityCredential/constants.ts +++ b/sdk/identity/identity/src/credentials/managedIdentityCredential/constants.ts @@ -3,7 +3,8 @@ export const DefaultScopeSuffix = "/.default"; -export const imdsEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"; +export const imdsHost = "http://169.254.169.254"; +export const imdsEndpointPath = "/metadata/identity/oauth2/token"; export const imdsApiVersion = "2018-02-01"; export const azureArcAPIVersion = "2019-11-01"; export const azureFabricVersion = "2019-07-01-preview"; diff --git a/sdk/identity/identity/src/credentials/managedIdentityCredential/imdsMsi.ts b/sdk/identity/identity/src/credentials/managedIdentityCredential/imdsMsi.ts index 9f6ee8dc5526..6610e04baf43 100644 --- a/sdk/identity/identity/src/credentials/managedIdentityCredential/imdsMsi.ts +++ b/sdk/identity/identity/src/credentials/managedIdentityCredential/imdsMsi.ts @@ -7,13 +7,14 @@ import { createHttpHeaders, PipelineRequestOptions, createPipelineRequest, + RawHttpHeaders, RestError } from "@azure/core-rest-pipeline"; import { SpanStatusCode } from "@azure/core-tracing"; import { IdentityClient } from "../../client/identityClient"; import { credentialLogger } from "../../util/logging"; import { createSpan } from "../../util/tracing"; -import { imdsApiVersion, imdsEndpoint } from "./constants"; +import { imdsApiVersion, imdsHost, imdsEndpointPath } from "./constants"; import { MSI } from "./models"; import { msiGenericGetToken } from "./utils"; @@ -33,7 +34,14 @@ function expiresInParser(requestBody: any): number { } } -function prepareRequestOptions(resource?: string, clientId?: string): PipelineRequestOptions { +function prepareRequestOptions( + resource?: string, + clientId?: string, + options?: { + skipQuery?: boolean; + skipMetadataHeader?: boolean; + } +): PipelineRequestOptions { const queryParameters: any = { resource, "api-version": imdsApiVersion @@ -43,15 +51,30 @@ function prepareRequestOptions(resource?: string, clientId?: string): PipelineRe queryParameters.client_id = clientId; } - const query = qs.stringify(queryParameters); + const url = new URL(imdsEndpointPath, process.env.AZURE_POD_IDENTITY_AUTHORITY_HOST ?? imdsHost); + + const { skipQuery, skipMetadataHeader } = options || {}; + + // Pod Identity will try to process this request even if the Metadata header is missing. + // We can exclude the request query to ensure no IMDS endpoint tries to process the ping request. + let query = ""; + if (!skipQuery) { + query = `?${qs.stringify(queryParameters)}`; + } + + const headersSource: RawHttpHeaders = { + Accept: "application/json", + Metadata: "true" + }; + // Remove the Metadata header to invoke a request error from some IMDS endpoints. + if (skipMetadataHeader) { + delete headersSource.Metadata; + } return { - url: `${imdsEndpoint}?${query}`, + url: `${url}${query}`, method: "GET", - headers: createHttpHeaders({ - Accept: "application/json", - Metadata: "true" - }) + headers: createHttpHeaders(headersSource) }; } @@ -62,29 +85,29 @@ export const imdsMsi: MSI = { clientId?: string, getTokenOptions?: GetTokenOptions ): Promise { + // if the PodIdenityEndpoint environment variable was set no need to probe the endpoint, it can be assumed to exist + if (process.env.AZURE_POD_IDENTITY_AUTHORITY_HOST) { + return true; + } + const { span, updatedOptions: options } = createSpan( "ManagedIdentityCredential-pingImdsEndpoint", getTokenOptions ); - const requestOptions = prepareRequestOptions(resource, clientId); - - // This will always be populated, but let's make TypeScript happy - if (requestOptions.headers) { - // Remove the Metadata header to invoke a request error from - // IMDS endpoint - requestOptions.headers.delete("Metadata"); - } - - requestOptions.tracingOptions = { - spanOptions: options.tracingOptions && options.tracingOptions.spanOptions, - tracingContext: options.tracingOptions && options.tracingOptions.tracingContext - }; - try { // Create a request with a timeout since we expect that // not having a "Metadata" header should cause an error to be // returned quickly from the endpoint, proving its availability. + // Later we found that skipping the query parameters is also necessary in some cases. + const requestOptions = prepareRequestOptions(resource, clientId, { + skipMetadataHeader: true, + skipQuery: true + }); + requestOptions.tracingOptions = { + spanOptions: options.tracingOptions && options.tracingOptions.spanOptions, + tracingContext: options.tracingOptions && options.tracingOptions.tracingContext + }; const request = createPipelineRequest(requestOptions); request.timeout = options.requestOptions?.timeout ?? 300; diff --git a/sdk/identity/identity/test/internal/node/managedIdentityCredential.spec.ts b/sdk/identity/identity/test/internal/node/managedIdentityCredential.spec.ts index e08cffd843ad..7eecdb06cc66 100644 --- a/sdk/identity/identity/test/internal/node/managedIdentityCredential.spec.ts +++ b/sdk/identity/identity/test/internal/node/managedIdentityCredential.spec.ts @@ -13,9 +13,11 @@ import { } from "../../public/node/nodeAuthTestUtils"; import { imdsApiVersion, - imdsEndpoint + imdsEndpointPath, + imdsHost } from "../../../src/credentials/managedIdentityCredential/constants"; import { assertRejects } from "../../authTestUtils"; +import { imdsMsi } from "../../../src/credentials/managedIdentityCredential/imdsMsi"; describe("ManagedIdentityCredential", function() { let testContext: IdentityTestContext; @@ -33,6 +35,7 @@ describe("ManagedIdentityCredential", function() { delete process.env.MSI_SECRET; delete process.env.IDENTITY_SERVER_THUMBPRINT; delete process.env.IMDS_ENDPOINT; + delete process.env.AZURE_POD_IDENTITY_AUTHORITY_HOST; }); it("sends an authorization request with a modified resource name", async function() { @@ -54,16 +57,24 @@ describe("ManagedIdentityCredential", function() { }); // The first request is the IMDS ping. + const imdsPingRequest = authDetails.insecureRequestOptions[0]; + assert.ok(!imdsPingRequest.headers!.metadata); + assert.equal(imdsPingRequest.path, imdsEndpointPath); + // The second one tries to authenticate against IMDS once we know the endpoint is available. const authRequest = authDetails.insecureRequestOptions[1]; + assert.ok(authRequest.headers!.metadata); + const query = qs.parse(authRequest.path!.split("?")[1]); assert.equal(authRequest.method, "GET"); assert.equal(query.client_id, "client"); assert.equal(decodeURIComponent(query.resource as string), "https://service"); assert.ok( - `http://${authRequest.hostname}${authRequest.path}`.startsWith(imdsEndpoint), + `http://${authRequest.hostname}${authRequest.path}`.startsWith( + `${imdsHost}${imdsEndpointPath}` + ), "URL does not start with expected host and path" ); assert.ok( @@ -198,6 +209,12 @@ describe("ManagedIdentityCredential", function() { assert.equal(authDetails.result!.token, "token"); }); + it("IMDS MSI skips verification if the AZURE_POD_IDENTITY_AUTHORITY_HOST environment variable is available", async function() { + process.env.AZURE_POD_IDENTITY_AUTHORITY_HOST = "token URL"; + + assert.ok(await imdsMsi.isAvailable()); + }); + // Unavailable exception throws while IMDS endpoint is unavailable. This test not valid. // it("can extend timeout for IMDS endpoint", async function() { // // Mock a timeout so that the endpoint ping fails