Skip to content
Merged
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
5 changes: 0 additions & 5 deletions eng/pipelines/templates/jobs/archetype-sdk-client.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ parameters:
- name: Matrix
type: object
default:
Linux_Node8:
Pool: $(LinuxPool)
OSVmImage:
NodeTestVersion: "8.x"
TestType: "node"
Comment thread
sadasant marked this conversation as resolved.
Windows_Node10:
Pool: $(WindowsPool)
OSVmImage:
Expand Down
4 changes: 3 additions & 1 deletion sdk/identity/identity/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
3 changes: 2 additions & 1 deletion sdk/identity/identity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
Comment thread
sadasant marked this conversation as resolved.
- **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

Expand All @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows to skip both or only one of the two. From my understanding of the PR description, we are skipping both to verify the endpoint. We keep both when getting the token. What is the scenario where we would need to skip only one of the two?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now we don’t have a scenario in which we might want to skip only one, but it felt worse for me to have some arbitrary parameter that would do more than one thing. Like skipDetails? It feels so constrained to some current vague definition. So, I went with being extra verbose on what’s the intention of this last optional parameter, in a way that can help us add more configuration later.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why I consider this a “concurrent definition” is because we assumed all IMDS endpoints would behave this way (rejecting requests without the Metadata header) up to this point. So, I want to make it easier for us to adapt to future findings, if they happen.

Copy link
Copy Markdown
Contributor Author

@sadasant sadasant Aug 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I made this hotfix, and I’m intending to release it asap, I’m in the process of communicating this unexpected behavior to the team. Charles confirmed to me that, given that this header is not as we thought it would be, a similar change seems necessary in other languages. It could lead to something interesting. But the code change seems an improvement regardless of future inquiries.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be you can highlight some where in the ref-docs for the user when to use these skip-options and in what combinations?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not part of the public API though. This is invisible to users. Well, except when their authentication sometimes fails (current scenario in k8s).

}
): PipelineRequestOptions {
const queryParameters: any = {
resource,
"api-version": imdsApiVersion
Expand All @@ -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)
};
}

Expand All @@ -62,29 +85,29 @@ export const imdsMsi: MSI = {
clientId?: string,
getTokenOptions?: GetTokenOptions
): Promise<boolean> {
// 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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() {
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Down