-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
[identity] add support for app service 2019 #20789
Changes from all commits
ee898bf
5a12b4a
6c8792c
500d6fb
d95f822
bbec799
871ba18
416c765
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
---|---|---|
|
@@ -558,6 +558,86 @@ describe("ManagedIdentityCredential", function () { | |
} | ||
}); | ||
|
||
it("sends an authorization request correctly in an App Service 2019 environment by client id", async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: Follow up with the crew in standup - do we need to remove support for App service 2017 version? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok turns out we should not remove 2017! So this PR is good 🙂 |
||
// 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"; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sadasant Can we go over the log warnings and info if they are appropriate for this? Also do we need any logs for
clientId
?