Skip to content

Commit

Permalink
[identity] add support for app service 2019 (Azure#20789)
Browse files Browse the repository at this point in the history
  • Loading branch information
KarishmaGhiya authored Mar 22, 2022
1 parent 5cf8e83 commit b1ac4ee
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 1 deletion.
1 change: 1 addition & 0 deletions sdk/identity/identity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Features Added

- Added support for App Service 2019 resource in Managed Identity Credential.
- All of our credentials now support a new option on their constructor: `loggingOptions`, which allows configuring the logging options of the HTTP pipelines.
- Within the new `loggingOptions` we have also added `allowLoggingAccountIdentifiers`, a property that if set to true logs information specific to the authenticated account after each successful authentication, including: the Client ID, the Tenant ID, the Object ID of the authenticated user, and if possible the User Principal Name.
- Added `disableAuthorityValidation`, which allows passing any `authorityHost` regardless of whether it can be validated or not. This is specially useful in private clouds.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import {
createHttpHeaders,
createPipelineRequest,
PipelineRequestOptions,
} from "@azure/core-rest-pipeline";
import { AccessToken, GetTokenOptions } from "@azure/core-auth";
import { TokenResponseParsedBody } from "../../client/identityClient";
import { credentialLogger } from "../../util/logging";
import { MSI, MSIConfiguration } from "./models";
import { mapScopesToResource } from "./utils";

const msiName = "ManagedIdentityCredential - AppServiceMSI 2019";
const logger = credentialLogger(msiName);

/**
* Formats the expiration date of the received token into the number of milliseconds between that date and midnight, January 1, 1970.
*/
function expiresOnParser(requestBody: TokenResponseParsedBody): number {
// App Service always returns string expires_on values.
return Date.parse(requestBody.expires_on! as string);
}

/**
* Generates the options used on the request for an access token.
*/
function prepareRequestOptions(
scopes: string | string[],
clientId?: string,
resourceId?: string
): PipelineRequestOptions {
const resource = mapScopesToResource(scopes);
if (!resource) {
throw new Error(`${msiName}: Multiple scopes are not supported.`);
}

const queryParameters: Record<string, string> = {
resource,
"api-version": "2019-08-01",
};

if (clientId) {
queryParameters.client_id = clientId;
}

if (resourceId) {
queryParameters.mi_res_id = resourceId;
}
const query = new URLSearchParams(queryParameters);

// This error should not bubble up, since we verify that this environment variable is defined in the isAvailable() method defined below.
if (!process.env.IDENTITY_ENDPOINT) {
throw new Error(`${msiName}: Missing environment variable: IDENTITY_ENDPOINT`);
}
if (!process.env.IDENTITY_HEADER) {
throw new Error(`${msiName}: Missing environment variable: IDENTITY_HEADER`);
}

return {
url: `${process.env.IDENTITY_ENDPOINT}?${query.toString()}`,
method: "GET",
headers: createHttpHeaders({
Accept: "application/json",
"X-IDENTITY-HEADER": process.env.IDENTITY_HEADER,
}),
};
}

/**
* Defines how to determine whether the Azure App Service MSI is available, and also how to retrieve a token from the Azure App Service MSI.
*/
export const appServiceMsi2019: MSI = {
async isAvailable({ scopes }): Promise<boolean> {
const resource = mapScopesToResource(scopes);
if (!resource) {
logger.info(`${msiName}: Unavailable. Multiple scopes are not supported.`);
return false;
}
const env = process.env;
const result = Boolean(env.IDENTITY_ENDPOINT && env.IDENTITY_HEADER);
if (!result) {
logger.info(
`${msiName}: Unavailable. The environment variables needed are: IDENTITY_ENDPOINT and IDENTITY_HEADER.`
);
}
return result;
},
async getToken(
configuration: MSIConfiguration,
getTokenOptions: GetTokenOptions = {}
): Promise<AccessToken | null> {
const { identityClient, scopes, clientId, resourceId } = configuration;

logger.info(
`${msiName}: Using the endpoint and the secret coming form the environment variables: IDENTITY_ENDPOINT=${process.env.IDENTITY_ENDPOINT} and IDENTITY_HEADER=[REDACTED].`
);

const request = createPipelineRequest({
abortSignal: getTokenOptions.abortSignal,
...prepareRequestOptions(scopes, clientId, resourceId),
// Generally, MSI endpoints use the HTTP protocol, without transport layer security (TLS).
allowInsecureConnection: true,
});
const tokenResponse = await identityClient.sendTokenRequest(request, expiresOnParser);
return (tokenResponse && tokenResponse.accessToken) || null;
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { MSI } from "./models";
import { arcMsi } from "./arcMsi";
import { tokenExchangeMsi } from "./tokenExchangeMsi";
import { fabricMsi } from "./fabricMsi";
import { appServiceMsi2019 } from "./appServiceMsi2019";

const logger = credentialLogger("ManagedIdentityCredential");

Expand Down Expand Up @@ -123,7 +124,15 @@ export class ManagedIdentityCredential implements TokenCredential {
return this.cachedMSI;
}

const MSIs = [fabricMsi, appServiceMsi2017, cloudShellMsi, arcMsi, tokenExchangeMsi(), imdsMsi];
const MSIs = [
fabricMsi,
appServiceMsi2019,
appServiceMsi2017,
cloudShellMsi,
arcMsi,
tokenExchangeMsi(),
imdsMsi,
];

for (const msi of MSIs) {
if (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,86 @@ describe("ManagedIdentityCredential", function () {
}
});

it("sends an authorization request correctly in an App Service 2019 environment by client id", async () => {
// Trigger App Service behavior by setting environment variables
process.env.IDENTITY_ENDPOINT = "https://endpoint";
process.env.IDENTITY_HEADER = "HEADER";

const authDetails = await testContext.sendCredentialRequests({
scopes: ["https://service/.default"],
credential: new ManagedIdentityCredential("client"),
secureResponses: [
createResponse(200, {
access_token: "token",
expires_on: "06/20/2021 02:57:58 +00:00",
}),
],
});

const authRequest = authDetails.requests[0];
console.log(`authDetails = ${authDetails}`);
console.log(`authRequest = ${authRequest}`);
const query = new URLSearchParams(authRequest.url.split("?")[1]);

assert.equal(authRequest.method, "GET");
assert.equal(query.get("client_id"), "client");
assert.equal(decodeURIComponent(query.get("resource")!), "https://service");
assert.ok(
authRequest.url.startsWith(process.env.IDENTITY_ENDPOINT),
"URL does not start with expected host and path"
);
assert.equal(authRequest.headers["X-IDENTITY-HEADER"], process.env.IDENTITY_HEADER);
assert.ok(
authRequest.url.indexOf(`api-version=2019-08-01`) > -1,
"URL does not have expected version"
);
if (authDetails.result?.token) {
assert.equal(authDetails.result.expiresOnTimestamp, 1624157878000);
} else {
assert.fail("No token was returned!");
}
});

it("sends an authorization request correctly in an App Service 2019 environment by resource id", async () => {
// Trigger App Service behavior by setting environment variables
process.env.IDENTITY_ENDPOINT = "https://endpoint";
process.env.IDENTITY_HEADER = "HEADER";

const authDetails = await testContext.sendCredentialRequests({
scopes: ["https://service/.default"],
credential: new ManagedIdentityCredential({ resourceId: "RESOURCE-ID" }),
secureResponses: [
createResponse(200, {
access_token: "token",
expires_on: "06/20/2021 02:57:58 +00:00",
}),
],
});

const authRequest = authDetails.requests[0];
console.log(`authDetails = ${authDetails}`);
console.log(`authRequest = ${authRequest}`);
const query = new URLSearchParams(authRequest.url.split("?")[1]);

assert.equal(authRequest.method, "GET");
assert.equal(decodeURIComponent(query.get("resource")!), "https://service");
assert.equal(decodeURIComponent(query.get("mi_res_id")!), "RESOURCE-ID");
assert.ok(
authRequest.url.startsWith(process.env.IDENTITY_ENDPOINT),
"URL does not start with expected host and path"
);
assert.equal(authRequest.headers["X-IDENTITY-HEADER"], process.env.IDENTITY_HEADER);
assert.ok(
authRequest.url.indexOf(`api-version=2019-08-01`) > -1,
"URL does not have expected version"
);
if (authDetails.result?.token) {
assert.equal(authDetails.result.expiresOnTimestamp, 1624157878000);
} else {
assert.fail("No token was returned!");
}
});

it("sends an authorization request correctly in an Cloud Shell environment", async () => {
// Trigger Cloud Shell behavior by setting environment variables
process.env.MSI_ENDPOINT = "https://endpoint";
Expand Down

0 comments on commit b1ac4ee

Please sign in to comment.