Skip to content
This repository was archived by the owner on Nov 4, 2021. It is now read-only.

Commit 399d3d6

Browse files
kpajdzikamarzavery
authored andcommittedSep 5, 2018
Update support for MSI authentication (Azure#13)
* Add TSLint rule to limit classes per file to one * Add d.ts and js.map files to gitignore * Ignore dist directory * Add empty MSI credential classes * Update MSIOptions and MSIAppServiceOptions * Update MsiTokenCredentials * Add implementation of MSI* credentials * Fix extra whitespace * Add typings for ADAL library * Fix TSLint credential issues * Temporary commit for fixing overloads * Fix _withMSI declaration * Fix loginWithMSI overloads * Fix loginWithVmMSI overloads * Fix _withMSI declaration * Fix loginWithAppServiceMSI overloads * Fix _withAppServiceMSI declaration * Fix missing TypeScript types in MSI logging methods * Make MSITokenCredentials abstract * Fix TSLint problems in token credential classes * Remove loginWithMSI method * Remove dist/lib files from tracking * Bootstrap test directory * Add unit tests * Add tokenClientCredentials class * Update exports * Fix extra whitespace * Move options to corresponding classes * Adjusting types to newer model * Updating tests * Fix compiler errors in unit tests * Enable injecting custom HttpClient to MSI credentials classses * Update MSI tests * Fix promise return type * Fix tests * Uncomment tests * Remove unused package * Update headers to contain MIT license * Remove commented code * Update documentation * Fix slashes in RM endpoint * Rempve @types/adal-angular package * Update missing documentation * Update README.md
1 parent ef62ed1 commit 399d3d6

29 files changed

+2758
-1311
lines changed
 

‎.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,10 @@ jspm_packages/
5454
# dotenv environment variables file
5555
.env
5656

57+
# Typescript output
58+
.nyc_output/
59+
coverage/
60+
dist/
61+
typings/
62+
*.js
63+
*.js.map

‎.vscode/launch.json

+26-10
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,33 @@
11
{
2-
// Use IntelliSense to learn about possible Node.js debug attributes.
3-
// Hover to view descriptions of existing attributes.
4-
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
52
"version": "0.2.0",
63
"configurations": [
74
{
8-
"type": "node",
9-
"request": "launch",
10-
"name": "Launch Program",
11-
"program": "${workspaceRoot}/samples/sample.ts",
12-
"outFiles": [
13-
"${workspaceRoot}/dist/**/*.js"
14-
]
5+
"type": "node",
6+
"request": "launch",
7+
"name": "Mocha All",
8+
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
9+
"args": [
10+
"--timeout",
11+
"999999",
12+
"--colors",
13+
"${workspaceFolder}/test"
14+
],
15+
"console": "integratedTerminal",
16+
"internalConsoleOptions": "neverOpen"
17+
},
18+
{
19+
"type": "node",
20+
"request": "launch",
21+
"name": "Mocha Current File",
22+
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
23+
"args": [
24+
"--timeout",
25+
"999999",
26+
"--colors",
27+
"${file}"
28+
],
29+
"console": "integratedTerminal",
30+
"internalConsoleOptions": "neverOpen"
1531
}
1632
]
1733
}

‎README.md

+22-2
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,31 @@ msRestNodeAuth.loginWithAuthFileWithAuthResponse(options).then((authRes) => {
5858
});
5959
```
6060

61-
### MSI(Managed Service Identity) based login from a virtual machine created in Azure.
61+
### MSI (Managed Service Identity) based login from a virtual machine created in Azure.
6262
```typescript
6363
import * as msRestNodeAuth from "../lib/msRestNodeAuth";
6464

65-
msRestNodeAuth.loginWithMSI("your-tenantId").then((msiTokenRes) => {
65+
const options: msRestNodeAuth.MSIVmOptions = {
66+
port: 50342;
67+
}
68+
69+
msRestNodeAuth.loginWithVmMSI(options).then((msiTokenRes) => {
70+
console.log(msiTokenRes);
71+
}).catch((err) => {
72+
console.log(err);
73+
});
74+
```
75+
76+
77+
### MSI (Managed Service Identity) based login from an AppService or Azure Function created in Azure.
78+
```typescript
79+
import * as msRestNodeAuth from "../lib/msRestNodeAuth";
80+
81+
const options: msRestNodeAuth.MSIAppServiceOptions = {
82+
msiEndpoint: "http://127.0.0.1:41741/MSI/token/";
83+
}
84+
85+
msRestNodeAuth.loginWithAppServiceMSI(options).then((msiTokenRes) => {
6686
console.log(msiTokenRes);
6787
}).catch((err) => {
6888
console.log(err);

‎dist/lib/credentials/applicationTokenCredentials.js

-104
This file was deleted.

‎dist/lib/credentials/deviceTokenCredentials.js

-45
This file was deleted.

‎dist/lib/credentials/msiTokenCredentials.js

-102
This file was deleted.

‎dist/lib/credentials/tokenCredentialsBase.js

-77
This file was deleted.

‎dist/lib/credentials/userTokenCredentials.js

-84
This file was deleted.

‎dist/lib/login.js

-524
This file was deleted.

‎dist/lib/msRestNodeAuth.js

-27
This file was deleted.

‎dist/lib/subscriptionManagement/subscriptionUtils.js

-98
This file was deleted.

‎dist/lib/util/authConstants.js

-12
This file was deleted.

‎lib/credentials/applicationTokenCredentials.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4-
import { TokenCredentialsBase, TokenResponse } from "./tokenCredentialsBase";
4+
import { TokenCredentialsBase } from "./tokenCredentialsBase";
55
import { AzureEnvironment } from "ms-rest-azure-env";
66
import { AuthConstants, TokenAudience } from "../util/authConstants";
7+
import { TokenResponse } from "adal-node";
78

89
export class ApplicationTokenCredentials extends TokenCredentialsBase {
910

‎lib/credentials/deviceTokenCredentials.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4-
import { TokenCredentialsBase, TokenResponse } from "./tokenCredentialsBase";
4+
import { TokenCredentialsBase } from "./tokenCredentialsBase";
55
import { AzureEnvironment } from "ms-rest-azure-env";
66
import { AuthConstants, TokenAudience } from "../util/authConstants";
7+
import { TokenResponse } from "adal-node";
78

89
export class DeviceTokenCredentials extends TokenCredentialsBase {
910

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
import { MSITokenCredentials, MSIOptions, MSITokenResponse } from "./msiTokenCredentials";
5+
import { HttpOperationResponse, RequestPrepareOptions, WebResource } from "ms-rest-js";
6+
7+
/**
8+
* @interface MSIAppServiceOptions Defines the optional parameters for authentication with MSI for AppService.
9+
*/
10+
export interface MSIAppServiceOptions extends MSIOptions {
11+
/**
12+
* @property {string} [msiEndpoint] - The local URL from which your app can request tokens.
13+
* Either provide this parameter or set the environment varaible `MSI_ENDPOINT`.
14+
* For example: `export MSI_ENDPOINT="http://127.0.0.1:41741/MSI/token/"`
15+
*/
16+
msiEndpoint?: string;
17+
/**
18+
* @property {string} [msiSecret] - The secret used in communication between your code and the local MSI agent.
19+
* Either provide this parameter or set the environment varaible `MSI_SECRET`.
20+
* For example: `export MSI_SECRET="69418689F1E342DD946CB82994CDA3CB"`
21+
*/
22+
msiSecret?: string;
23+
/**
24+
* @property {string} [msiApiVersion] - The api-version of the local MSI agent. Default value is "2017-09-01".
25+
*/
26+
msiApiVersion?: string;
27+
}
28+
29+
/**
30+
* @class MSIAppServiceTokenCredentials
31+
*/
32+
export class MSIAppServiceTokenCredentials extends MSITokenCredentials {
33+
/**
34+
* @property {string} msiEndpoint - The local URL from which your app can request tokens.
35+
* Either provide this parameter or set the environment varaible `MSI_ENDPOINT`.
36+
* For example: `MSI_ENDPOINT="http://127.0.0.1:41741/MSI/token/"`
37+
*/
38+
msiEndpoint: string;
39+
/**
40+
* @property {string} msiSecret - The secret used in communication between your code and the local MSI agent.
41+
* Either provide this parameter or set the environment varaible `MSI_SECRET`.
42+
* For example: `MSI_SECRET="69418689F1E342DD946CB82994CDA3CB"`
43+
*/
44+
msiSecret: string;
45+
/**
46+
* @property {string} [msiApiVersion] The api-version of the local MSI agent. Default value is "2017-09-01".
47+
*/
48+
msiApiVersion?: string;
49+
50+
/**
51+
* Creates an instance of MSIAppServiceTokenCredentials.
52+
* @param {string} [options.msiEndpoint] - The local URL from which your app can request tokens.
53+
* Either provide this parameter or set the environment varaible `MSI_ENDPOINT`.
54+
* For example: `MSI_ENDPOINT="http://127.0.0.1:41741/MSI/token/"`
55+
* @param {string} [options.msiSecret] - The secret used in communication between your code and the local MSI agent.
56+
* Either provide this parameter or set the environment varaible `MSI_SECRET`.
57+
* For example: `MSI_SECRET="69418689F1E342DD946CB82994CDA3CB"`
58+
* @param {string} [options.resource] - The resource uri or token audience for which the token is needed.
59+
* For e.g. it can be:
60+
* - resource management endpoint "https://management.azure.com/" (default)
61+
* - management endpoint "https://management.core.windows.net/"
62+
* @param {string} [options.msiApiVersion] - The api-version of the local MSI agent. Default value is "2017-09-01".
63+
*/
64+
constructor(options?: MSIAppServiceOptions) {
65+
if (!options) options = {};
66+
super(options);
67+
options.msiEndpoint = options.msiEndpoint || process.env["MSI_ENDPOINT"];
68+
options.msiSecret = options.msiSecret || process.env["MSI_SECRET"];
69+
if (!options.msiEndpoint || (options.msiEndpoint && typeof options.msiEndpoint.valueOf() !== "string")) {
70+
throw new Error('Either provide "msiEndpoint" as a property of the "options" object ' +
71+
'or set the environment variable "MSI_ENDPOINT" and it must be of type "string".');
72+
}
73+
74+
if (!options.msiSecret || (options.msiSecret && typeof options.msiSecret.valueOf() !== "string")) {
75+
throw new Error('Either provide "msiSecret" as a property of the "options" object ' +
76+
'or set the environment variable "MSI_SECRET" and it must be of type "string".');
77+
}
78+
79+
if (!options.msiApiVersion) {
80+
options.msiApiVersion = "2017-09-01";
81+
} else if (typeof options.msiApiVersion.valueOf() !== "string") {
82+
throw new Error("msiApiVersion must be a uri.");
83+
}
84+
85+
this.msiEndpoint = options.msiEndpoint;
86+
this.msiSecret = options.msiSecret;
87+
this.msiApiVersion = options.msiApiVersion;
88+
}
89+
90+
/**
91+
* Prepares and sends a POST request to a service endpoint hosted on the Azure VM, which responds with the access token.
92+
* @param {function} callback The callback in the form (err, result)
93+
* @return {function} callback
94+
* {Error} [err] The error if any
95+
* {object} [tokenResponse] The tokenResponse (tokenType and accessToken are the two important properties).
96+
*/
97+
async getToken(): Promise<MSITokenResponse> {
98+
const reqOptions = this.prepareRequestOptions();
99+
let opRes: HttpOperationResponse;
100+
let result: MSITokenResponse;
101+
102+
opRes = await this.httpClient.sendRequest(reqOptions);
103+
if (opRes.bodyAsText === undefined || opRes.bodyAsText!.indexOf("ExceptionMessage") !== -1) {
104+
throw new Error(`MSI: Failed to retrieve a token from "${reqOptions.url}" with an error: ${opRes.bodyAsText}`);
105+
}
106+
107+
result = this.parseTokenResponse(opRes.bodyAsText!) as MSITokenResponse;
108+
if (!result.tokenType) {
109+
throw new Error(`Invalid token response, did not find tokenType. Response body is: ${opRes.bodyAsText}`);
110+
} else if (!result.accessToken) {
111+
throw new Error(`Invalid token response, did not find accessToken. Response body is: ${opRes.bodyAsText}`);
112+
}
113+
114+
return result;
115+
}
116+
117+
protected prepareRequestOptions(): WebResource {
118+
const endpoint = this.msiEndpoint.endsWith("/") ? this.msiEndpoint : `${this.msiEndpoint}/`;
119+
const getUrl = `${endpoint}?resource=${this.resource}&api-version=${this.msiApiVersion}`;
120+
const resource = encodeURIComponent(this.resource);
121+
const reqOptions: RequestPrepareOptions = {
122+
url: getUrl,
123+
headers: {
124+
"secret": this.msiSecret
125+
},
126+
body: `resource=${resource}`,
127+
method: "POST"
128+
};
129+
130+
const webResource = new WebResource();
131+
return webResource.prepare(reqOptions);
132+
}
133+
}
+91-64
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,33 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4-
import * as msRest from "ms-rest-js";
5-
import { MSIOptions } from "../login";
6-
7-
const defaultPort = 50342;
8-
const defaultResource = "https://management.azure.com/";
4+
import { Constants, WebResource, HttpClient, DefaultHttpClient } from "ms-rest-js";
5+
import { TokenClientCredentials, TokenResponse } from "./tokenClientCredentials";
6+
import { AuthConstants } from "../util/authConstants";
97

108
/**
11-
* @interface MSITokenResponse - Describes the MSITokenResponse.
9+
* @interface MSIOptions Defines the optional parameters for authentication with MSI.
1210
*/
13-
export interface MSITokenResponse {
11+
export interface MSIOptions {
1412
/**
15-
* @property {string} token_type - The token type.
13+
* @prop {string} [resource] - The resource uri or token audience for which the token is needed.
14+
* For e.g. it can be:
15+
* - resourcemanagement endpoint "https://management.azure.com/" (default)
16+
* - management endpoint "https://management.core.windows.net/"
1617
*/
17-
readonly token_type: string;
18+
resource?: string;
19+
1820
/**
19-
* @property {string} access_token - The access token.
21+
* @property {HttpClient} [httpClient] - The client responsible for sending HTTP requests.
22+
* By default it is Axios-based {@link DefaultHttpClient}.
2023
*/
21-
readonly access_token: string;
24+
httpClient?: HttpClient;
25+
}
26+
27+
/**
28+
* @interface MSITokenResponse - Describes the MSITokenResponse.
29+
*/
30+
export interface MSITokenResponse extends TokenResponse {
2231
/**
2332
* @property {any} any - Placeholder for unknown properties.
2433
*/
@@ -29,76 +38,94 @@ export interface MSITokenResponse {
2938
* @class MSITokenCredentials - Provides information about managed service identity token credentials.
3039
* This object can only be used to acquire token on a virtual machine provisioned in Azure with managed service identity.
3140
*/
32-
export class MSITokenCredentials {
33-
34-
35-
port: number;
41+
export abstract class MSITokenCredentials implements TokenClientCredentials {
3642
resource: string;
43+
protected httpClient: HttpClient;
3744

38-
public constructor(
39-
/**
40-
* @property {LoginWithMSIOptions} options Optional parameters
41-
*/
42-
public options: MSIOptions) {
43-
45+
/**
46+
* Creates an instance of MSITokenCredentials.
47+
* @param {object} [options] - Optional parameters
48+
* @param {string} [options.resource] - The resource uri or token audience for which the token is needed.
49+
* For e.g. it can be:
50+
* - resource management endpoint "https://management.azure.com/"(default)
51+
* - management endpoint "https://management.core.windows.net/"
52+
*/
53+
constructor(options: MSIOptions) {
4454
if (!options) options = {};
4555

46-
if (options.port === undefined) {
47-
options.port = defaultPort;
48-
} else if (typeof options.port !== "number") {
49-
throw new Error("port must be a number.");
50-
}
51-
5256
if (!options.resource) {
53-
options.resource = defaultResource;
57+
options.resource = AuthConstants.RESOURCE_MANAGER_ENDPOINT;
5458
} else if (typeof options.resource.valueOf() !== "string") {
5559
throw new Error("resource must be a uri.");
5660
}
5761

58-
this.port = options.port;
5962
this.resource = options.resource;
63+
this.httpClient = options.httpClient || new DefaultHttpClient();
6064
}
6165

6266
/**
63-
* Prepares and sends a POST request to a service endpoint hosted on the Azure VM, which responds with the access token.
64-
* @param {function} callback The callback in the form (err, result)
65-
* @return {function} callback
66-
* {Error} [err] The error if any
67-
* {object} [tokenResponse] The tokenResponse (token_type and access_token are the two important properties).
67+
* Parses a tokenResponse json string into a object, and converts properties on the first level to camelCase.
68+
* This method tries to standardize the tokenResponse
69+
* @param {string} body A json string
70+
* @return {object} [tokenResponse] The tokenResponse (tokenType and accessToken are the two important properties).
6871
*/
69-
async getToken(): Promise<MSITokenResponse> {
70-
const reqOptions = this.prepareRequestOptions();
71-
const client = new msRest.ServiceClient();
72-
let opRes: msRest.HttpOperationResponse;
73-
let result: MSITokenResponse;
74-
try {
75-
opRes = await client.sendRequest(reqOptions);
76-
result = opRes.parsedBody as MSITokenResponse;
77-
if (!result.token_type) {
78-
throw new Error(`Invalid token response, did not find token_type. Response body is: ${opRes.bodyAsText}`);
79-
} else if (!result.access_token) {
80-
throw new Error(`Invalid token response, did not find access_token. Response body is: ${opRes.bodyAsText}`);
72+
parseTokenResponse(body: string): TokenResponse {
73+
// Docs show different examples of possible MSI responses for different services. https://docs.microsoft.com/en-us/azure/active-directory/managed-service-identity/overview
74+
// expires_on - is a Date like string in this doc
75+
// - https://docs.microsoft.com/en-us/azure/app-service/app-service-managed-service-identity#rest-protocol-examples
76+
// In other doc it is stringified number.
77+
// - https://docs.microsoft.com/en-us/azure/active-directory/managed-service-identity/tutorial-linux-vm-access-arm#get-an-access-token-using-the-vms-identity-and-use-it-to-call-resource-manager
78+
const parsedBody = JSON.parse(body);
79+
parsedBody.accessToken = parsedBody["access_token"];
80+
delete parsedBody["access_token"];
81+
parsedBody.tokenType = parsedBody["token_type"];
82+
delete parsedBody["token_type"];
83+
if (parsedBody["refresh_token"]) {
84+
parsedBody.refreshToken = parsedBody["refresh_token"];
85+
delete parsedBody["refresh_token"];
86+
}
87+
if (parsedBody["expires_in"]) {
88+
parsedBody.expiresIn = parsedBody["expires_in"];
89+
if (typeof parsedBody["expires_in"] === "string") {
90+
// normal number as a string '1504130527'
91+
parsedBody.expiresIn = parseInt(parsedBody["expires_in"], 10);
92+
}
93+
delete parsedBody["expires_in"];
94+
}
95+
if (parsedBody["not_before"]) {
96+
parsedBody.notBefore = parsedBody["not_before"];
97+
if (typeof parsedBody["not_before"] === "string") {
98+
// normal number as a string '1504130527'
99+
parsedBody.notBefore = parseInt(parsedBody["not_before"], 10);
100+
}
101+
delete parsedBody["not_before"];
102+
}
103+
if (parsedBody["expires_on"]) {
104+
parsedBody.expiresOn = parsedBody["expires_on"];
105+
if (typeof parsedBody["expires_on"] === "string") {
106+
// possibly a Date string '09/14/2017 00:00:00 PM +00:00'
107+
if (parsedBody["expires_on"].includes(":") || parsedBody["expires_on"].includes("/")) {
108+
parsedBody.expiresOn = new Date(parsedBody["expires_on"], 10);
109+
} else {
110+
// normal number as a string '1504130527'
111+
parsedBody.expiresOn = new Date(parseInt(parsedBody["expires_on"], 10));
112+
}
81113
}
82-
} catch (err) {
83-
return Promise.reject(err);
114+
delete parsedBody["expires_on"];
84115
}
85-
return Promise.resolve(result);
116+
return parsedBody;
86117
}
87118

88-
private prepareRequestOptions(): msRest.RequestPrepareOptions {
89-
const resource = encodeURIComponent(this.resource);
90-
const reqOptions: msRest.RequestPrepareOptions = {
91-
url: `http://localhost:${this.port}/oauth2/token`,
92-
headers: {
93-
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
94-
"Metadata": "true"
95-
},
96-
body: `resource=${resource}`,
97-
method: "POST"
98-
};
119+
/**
120+
* Prepares and sends a POST request to a service endpoint hosted on the Azure VM, which responds with the access token.
121+
* @param {function} callback The callback in the form (err, result)
122+
* @return {function} callback
123+
* {Error} [err] The error if any
124+
* {object} [tokenResponse] The tokenResponse (tokenType and accessToken are the two important properties).
125+
*/
126+
abstract async getToken(): Promise<MSITokenResponse>;
99127

100-
return reqOptions;
101-
}
128+
protected abstract prepareRequestOptions(): WebResource;
102129

103130
/**
104131
* Signs a request with the Authentication header.
@@ -107,9 +134,9 @@ export class MSITokenCredentials {
107134
* @param {function(error)} callback The callback function.
108135
* @return {undefined}
109136
*/
110-
public async signRequest(webResource: msRest.WebResource): Promise<msRest.WebResource> {
137+
public async signRequest(webResource: WebResource): Promise<WebResource> {
111138
const tokenResponse = await this.getToken();
112-
webResource.headers.set(msRest.Constants.HeaderConstants.AUTHORIZATION, `${tokenResponse.tokenType} ${tokenResponse.accessToken}`);
139+
webResource.headers.set(Constants.HeaderConstants.AUTHORIZATION, `${tokenResponse.tokenType} ${tokenResponse.accessToken}`);
113140
return Promise.resolve(webResource);
114141
}
115142
}
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
import { MSITokenCredentials, MSIOptions, MSITokenResponse } from "./msiTokenCredentials";
5+
import { RequestPrepareOptions, HttpOperationResponse, WebResource } from "ms-rest-js";
6+
7+
/**
8+
* @interface MSIVmOptions Defines the optional parameters for authentication with MSI for Virtual Machine.
9+
*/
10+
export interface MSIVmOptions extends MSIOptions {
11+
/**
12+
* @prop {number} [port] - port on which the MSI service is running on the host VM. Default port is 50342
13+
*/
14+
port?: number;
15+
}
16+
17+
/**
18+
* @class MSIVmTokenCredentials
19+
*/
20+
export class MSIVmTokenCredentials extends MSITokenCredentials {
21+
port: number;
22+
23+
constructor(options?: MSIVmOptions) {
24+
if (!options) options = {};
25+
super(options);
26+
if (!options.port) {
27+
options.port = 50342; // default port where token service runs.
28+
} else if (typeof options.port !== "number") {
29+
throw new Error("port must be a number.");
30+
}
31+
32+
this.port = options.port;
33+
}
34+
35+
/**
36+
* Prepares and sends a POST request to a service endpoint hosted on the Azure VM, which responds with the access token.
37+
* @param {function} callback The callback in the form (err, result)
38+
* @return {function} callback
39+
* {Error} [err] The error if any
40+
* {object} [tokenResponse] The tokenResponse (tokenType and accessToken are the two important properties).
41+
*/
42+
async getToken(): Promise<MSITokenResponse> {
43+
const reqOptions = this.prepareRequestOptions();
44+
let opRes: HttpOperationResponse;
45+
let result: MSITokenResponse;
46+
47+
opRes = await this.httpClient.sendRequest(reqOptions);
48+
result = this.parseTokenResponse(opRes.bodyAsText!) as MSITokenResponse;
49+
if (!result.tokenType) {
50+
throw new Error(`Invalid token response, did not find tokenType. Response body is: ${opRes.bodyAsText}`);
51+
} else if (!result.accessToken) {
52+
throw new Error(`Invalid token response, did not find accessToken. Response body is: ${opRes.bodyAsText}`);
53+
}
54+
55+
56+
return result;
57+
}
58+
59+
protected prepareRequestOptions(): WebResource {
60+
const resource = encodeURIComponent(this.resource);
61+
const reqOptions: RequestPrepareOptions = {
62+
url: `http://localhost:${this.port}/oauth2/token`,
63+
headers: {
64+
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
65+
"Metadata": "true"
66+
},
67+
body: `resource=${resource}`,
68+
method: "POST"
69+
};
70+
71+
const webResource = new WebResource();
72+
return webResource.prepare(reqOptions);
73+
}
74+
}
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
import { ServiceClientCredentials } from "ms-rest-js";
5+
6+
export interface TokenResponse {
7+
readonly tokenType: string;
8+
readonly accessToken: string;
9+
readonly [x: string]: any;
10+
}
11+
12+
export interface TokenClientCredentials extends ServiceClientCredentials {
13+
getToken<TTokenResponse extends TokenResponse>(): Promise<TokenResponse | TTokenResponse>;
14+
}

‎lib/credentials/tokenCredentialsBase.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,11 @@
44
import { Constants as MSRestConstants, WebResource } from "ms-rest-js";
55
import { AzureEnvironment } from "ms-rest-azure-env";
66
import { TokenAudience } from "../util/authConstants";
7+
import { TokenClientCredentials } from "./tokenClientCredentials";
8+
import { TokenResponse } from "adal-node";
79
const adal = require("adal-node");
810

9-
export interface TokenResponse {
10-
readonly tokenType: string;
11-
readonly accessToken: string;
12-
readonly [x: string]: any;
13-
}
14-
15-
export abstract class TokenCredentialsBase {
11+
export abstract class TokenCredentialsBase implements TokenClientCredentials {
1612
protected readonly authContext: any;
1713

1814
public constructor(

‎lib/credentials/userTokenCredentials.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4-
import { TokenCredentialsBase, TokenResponse } from "./tokenCredentialsBase";
4+
import { TokenCredentialsBase } from "./tokenCredentialsBase";
55
import { AzureEnvironment } from "ms-rest-azure-env";
66
import { TokenAudience } from "../util/authConstants";
7+
import { TokenResponse } from "adal-node";
78

89
export class UserTokenCredentials extends TokenCredentialsBase {
910

@@ -81,7 +82,7 @@ export class UserTokenCredentials extends TokenCredentialsBase {
8182
if (error) {
8283
reject(error);
8384
}
84-
if (self.crossCheckUserNameWithToken(self.username, tokenResponse.userId)) {
85+
if (self.crossCheckUserNameWithToken(self.username, tokenResponse.userId!)) {
8586
resolve((tokenResponse as TokenResponse));
8687
} else {
8788
reject(`The userId "${tokenResponse.userId}" in access token doesn"t match the username "${self.username}" provided during authentication.`);

‎lib/login.ts

+100-41
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4-
const adal = require("adal-node");
4+
import * as adal from "adal-node";
55
import * as fs from "fs";
66
import * as msRest from "ms-rest-js";
77
import { AzureEnvironment } from "ms-rest-azure-env";
@@ -11,13 +11,15 @@ import { DeviceTokenCredentials } from "./credentials/deviceTokenCredentials";
1111
import { UserTokenCredentials } from "./credentials/userTokenCredentials";
1212
import { AuthConstants, TokenAudience } from "./util/authConstants";
1313
import { buildTenantList, getSubscriptionsFromTenants, LinkedSubscription } from "./subscriptionManagement/subscriptionUtils";
14-
import { MSITokenCredentials, MSITokenResponse } from "./credentials/msiTokenCredentials";
14+
import { MSIVmTokenCredentials, MSIVmOptions } from "./credentials/msiVmTokenCredentials";
15+
import { MSIAppServiceTokenCredentials, MSIAppServiceOptions } from "./credentials/msiAppServiceTokenCredentials";
16+
import { MSITokenResponse } from "./credentials/msiTokenCredentials";
1517

1618
function turnOnLogging() {
1719
const log = adal.Logging;
1820
log.setLoggingOptions(
1921
{
20-
level: log.LOGGING_LEVEL.VERBOSE,
22+
level: 3, // Please use log.LOGGING_LEVEL.VERBOSE once AD TypeScript mappings are updated,
2123
log: function (level: any, message: any, error: any) {
2224
level;
2325
console.info(message);
@@ -113,26 +115,12 @@ export interface LoginWithAuthFileOptions {
113115
}
114116

115117
/**
116-
* @interface MSIOptions - Describes optional parameters for MSI authentication.
118+
* Generic callback type definition.
119+
*
120+
* @property {Error} error - The error occurred if any, while executing the request; otherwise undefined
121+
* @property {TResult} result - Result when call was successful.
117122
*/
118-
export interface MSIOptions {
119-
/**
120-
* @property {number} port - Port on which the MSI service is running on the host VM. Default port is 50342
121-
*/
122-
port?: number;
123-
/**
124-
* @property {string} resource - The resource uri or token audience for which the token is needed.
125-
* For e.g. it can be:
126-
* - resourcemanagement endpoint "https://management.azure.com"(default)
127-
* - management endpoint "https://management.core.windows.net/"
128-
*/
129-
resource?: string;
130-
/**
131-
* @property {string} aadEndpoint - The add endpoint for authentication. default - "https://login.microsoftonline.com"
132-
*/
133-
aadEndpoint?: string;
134-
}
135-
123+
export type Callback<TResult> = (error?: Error, result?: TResult) => void;
136124
/**
137125
* Provides a UserTokenCredentials object and the list of subscriptions associated with that userId across all the applicable tenants.
138126
* This method is applicable only for organizational ids that are not 2FA enabled otherwise please use interactive login.
@@ -710,16 +698,24 @@ export function withUsernamePassword(username: string, password: string, options
710698
* @param {string} domain - required. The tenant id.
711699
* @param {object} options - Optional parameters
712700
* @param {string} [options.port] - port on which the MSI service is running on the host VM. Default port is 50342
713-
* @param {string} [options.resource] - The resource uri or token audience for which the token is needed. Default - "https://management.azure.com"
701+
* @param {string} [options.resource] - The resource uri or token audience for which the token is needed. Default - "https://management.azure.com/"
714702
* @param {string} [options.aadEndpoint] - The add endpoint for authentication. default - "https://login.microsoftonline.com"
715703
* @param {any} callback - the callback function.
716704
*/
717-
function _withMSI(domain: string, options?: MSIOptions): Promise<MSITokenResponse> {
705+
function _withMSI(options?: MSIVmOptions): Promise<MSIVmTokenCredentials> {
718706
if (!options) {
719707
options = {};
720708
}
721-
const creds = new MSITokenCredentials(domain, options.port, options.resource, options.aadEndpoint);
722-
return creds.getToken();
709+
710+
return new Promise((resolve, reject) => {
711+
const creds = new MSIVmTokenCredentials(options);
712+
creds.getToken().then((_tokenResponse) => {
713+
// We ignore the token response, it's put in the cache.
714+
return resolve(creds);
715+
}).catch(error => {
716+
reject(error);
717+
});
718+
});
723719
}
724720

725721
/**
@@ -741,43 +737,106 @@ function _withMSI(domain: string, options?: MSIOptions): Promise<MSITokenRespons
741737
* This method makes a request to the authentication service hosted on the VM
742738
* and gets back an access token.
743739
*
744-
* @param {string} [domain] - The domain or tenant id. This is a required parameter.
745740
* @param {object} [options] - Optional parameters
746741
* @param {string} [options.port] - port on which the MSI service is running on the host VM. Default port is 50342
747742
* @param {string} [options.resource] - The resource uri or token audience for which the token is needed.
748743
* For e.g. it can be:
749-
* - resourcemanagement endpoint "https://management.azure.com"(default)
744+
* - resourcemanagement endpoint "https://management.azure.com/"(default)
750745
* - management endpoint "https://management.core.windows.net/"
751-
* @param {string} [options.aadEndpoint] - The add endpoint for authentication. default - "https://login.microsoftonline.com"
752746
* @param {function} [optionalCallback] The optional callback.
753747
*
754748
* @returns {function | Promise} If a callback was passed as the last parameter then it returns the callback else returns a Promise.
755749
*
756750
* {function} optionalCallback(err, credentials)
757-
* {Error} [err] - The Error object if an error occurred, null otherwise.
758-
* {object} [tokenResponse] - The tokenResponse (token_type and access_token are the two important properties)
751+
* {Error} [err] - The Error object if an error occurred, null otherwise.
752+
* {object} [tokenResponse] - The tokenResponse (tokenType and accessToken are the two important properties)
759753
* {Promise} A promise is returned.
760-
* @resolve {MSITokenResponse} - The tokenResponse.
761-
* @reject {Error} - The error object.
754+
* @resolve {object} - tokenResponse.
755+
* @reject {Error} - error object.
762756
*/
763-
export function withMSI(domain: string): Promise<MSITokenResponse>;
764-
export function withMSI(domain: string, options: MSIOptions): Promise<MSITokenResponse>;
765-
export function withMSI(domain: string, options: MSIOptions, callback: { (err: Error, credentials: MSITokenResponse): void }): void;
766-
export function withMSI(domain: string, callback: any): any;
767-
export function withMSI(domain: string, options?: MSIOptions, callback?: { (err: Error, credentials: MSITokenResponse): void }): any {
757+
export function loginWithVmMSI(): Promise<MSIVmTokenCredentials>;
758+
export function loginWithVmMSI(options: MSIVmOptions): Promise<MSIVmTokenCredentials>;
759+
export function loginWithVmMSI(options: MSIVmOptions, callback: Callback<MSIVmTokenCredentials>): void;
760+
export function loginWithVmMSI(callback: Callback<MSIVmTokenCredentials>): void;
761+
export function loginWithVmMSI(options?: MSIVmOptions | Callback<MSIVmTokenCredentials>, callback?: Callback<MSIVmTokenCredentials>): void | Promise<MSIVmTokenCredentials> {
768762
if (!callback && typeof options === "function") {
769763
callback = options;
770764
options = {};
771765
}
772766
const cb = callback as Function;
773767
if (!callback) {
774-
return _withMSI(domain, options);
768+
return _withMSI(options as MSIVmOptions);
775769
} else {
776-
msRest.promiseToCallback(_withMSI(domain, options))((err: Error, tokenRes: MSITokenResponse) => {
770+
msRest.promiseToCallback(_withMSI(options as MSIVmOptions))((err: Error, tokenRes: MSITokenResponse) => {
777771
if (err) {
778772
return cb(err);
779773
}
780774
return cb(undefined, tokenRes);
781775
});
782776
}
783-
}
777+
}
778+
779+
/**
780+
* Private method
781+
*/
782+
function _withAppServiceMSI(options: MSIAppServiceOptions): Promise<MSIAppServiceTokenCredentials> {
783+
if (!options) {
784+
options = {};
785+
}
786+
787+
return new Promise((resolve, reject) => {
788+
const creds = new MSIAppServiceTokenCredentials(options);
789+
creds.getToken().then((_tokenResponse) => {
790+
// We ignore the token response, it's put in the cache.
791+
return resolve(creds);
792+
}).catch(error => {
793+
reject(error);
794+
});
795+
});
796+
}
797+
798+
/**
799+
* Authenticate using the App Service MSI.
800+
* @param {object} [options] - Optional parameters
801+
* @param {string} [options.msiEndpoint] - The local URL from which your app can request tokens.
802+
* Either provide this parameter or set the environment varaible `MSI_ENDPOINT`.
803+
* For example: `MSI_ENDPOINT="http://127.0.0.1:41741/MSI/token/"`
804+
* @param {string} [options.msiSecret] - The secret used in communication between your code and the local MSI agent.
805+
* Either provide this parameter or set the environment varaible `MSI_SECRET`.
806+
* For example: `MSI_SECRET="69418689F1E342DD946CB82994CDA3CB"`
807+
* @param {string} [options.resource] - The resource uri or token audience for which the token is needed.
808+
* For example, it can be:
809+
* - resourcemanagement endpoint "https://management.azure.com/"(default)
810+
* - management endpoint "https://management.core.windows.net/"
811+
* @param {string} [options.msiApiVersion] - The api-version of the local MSI agent. Default value is "2017-09-01".
812+
* @param {function} [optionalCallback] - The optional callback.
813+
* @returns {function | Promise} If a callback was passed as the last parameter then it returns the callback else returns a Promise.
814+
*
815+
* {function} optionalCallback(err, credentials)
816+
* {Error} [err] - The Error object if an error occurred, null otherwise.
817+
* {object} [tokenResponse] - The tokenResponse (tokenType and accessToken are the two important properties)
818+
* {Promise} A promise is returned.
819+
* @resolve {object} - tokenResponse.
820+
* @reject {Error} - error object.
821+
*/
822+
export function loginWithAppServiceMSI(): Promise<MSIAppServiceTokenCredentials>;
823+
export function loginWithAppServiceMSI(options: MSIAppServiceOptions): Promise<MSIAppServiceTokenCredentials>;
824+
export function loginWithAppServiceMSI(options: MSIAppServiceOptions, callback: Callback<MSIAppServiceTokenCredentials>): void;
825+
export function loginWithAppServiceMSI(callback: Callback<MSIAppServiceTokenCredentials>): void;
826+
export function loginWithAppServiceMSI(options?: MSIAppServiceOptions | Callback<MSIAppServiceTokenCredentials>, callback?: Callback<MSIAppServiceTokenCredentials>): void | Promise<MSIAppServiceTokenCredentials> {
827+
if (!callback && typeof options === "function") {
828+
callback = options;
829+
options = {};
830+
}
831+
const cb = callback as Function;
832+
if (!callback) {
833+
return _withAppServiceMSI(options as MSIAppServiceOptions);
834+
} else {
835+
msRest.promiseToCallback(_withAppServiceMSI(options as MSIAppServiceOptions))((err: Error, tokenRes: MSITokenResponse) => {
836+
if (err) {
837+
return cb(err);
838+
}
839+
return cb(undefined, tokenRes);
840+
});
841+
}
842+
}

‎lib/msRestNodeAuth.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4-
export { TokenCredentialsBase, TokenResponse } from "./credentials/tokenCredentialsBase";
4+
export { TokenCredentialsBase } from "./credentials/tokenCredentialsBase";
55
export { ApplicationTokenCredentials } from "./credentials/applicationTokenCredentials";
66
export { DeviceTokenCredentials } from "./credentials/deviceTokenCredentials";
77
export { UserTokenCredentials } from "./credentials/userTokenCredentials";
8-
export { MSITokenCredentials, MSITokenResponse } from "./credentials/msiTokenCredentials";
8+
export { MSIOptions, MSITokenCredentials, MSITokenResponse } from "./credentials/msiTokenCredentials";
9+
export { MSIAppServiceOptions, MSIAppServiceTokenCredentials } from "./credentials/msiAppServiceTokenCredentials";
10+
export { MSIVmOptions, MSIVmTokenCredentials } from "./credentials/msiVmTokenCredentials";
911
export { AuthConstants, TokenAudience } from "./util/authConstants";
1012
export { LinkedSubscription, LinkedUser, UserType } from "./subscriptionManagement/subscriptionUtils";
1113
export {
1214
AuthResponse, LoginWithAuthFileOptions, InteractiveLoginOptions,
13-
MSIOptions, AzureTokenCredentialsOptions, LoginWithUsernamePasswordOptions,
15+
AzureTokenCredentialsOptions, LoginWithUsernamePasswordOptions,
1416
interactive as interactiveLogin,
1517
withInteractiveWithAuthResponse as interactiveLoginWithAuthResponse,
1618
withUsernamePassword as loginWithUsernamePassword,
@@ -19,5 +21,4 @@ export {
1921
withServicePrincipalSecretWithAuthResponse as loginWithServicePrincipalSecretWithAuthResponse,
2022
withAuthFile as loginWithAuthFile,
2123
withAuthFileWithAuthResponse as loginWithAuthFileWithAuthResponse,
22-
withMSI as loginWithMSI,
2324
} from "./login";

‎lib/util/authConstants.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ export const AuthConstants = {
66
"DEFAULT_ADAL_CLIENT_ID": "04b07795-8ddb-461a-bbee-02f9e1bf7b46",
77
"SDK_INTERNAL_ERROR": "SDK_INTERNAL_ERROR",
88
"DEFAULT_LANGUAGE": "en-us",
9-
"AZURE_AUTH_LOCATION": "AZURE_AUTH_LOCATION"
9+
"AZURE_AUTH_LOCATION": "AZURE_AUTH_LOCATION",
10+
"RESOURCE_MANAGER_ENDPOINT": "https://management.azure.com/"
1011
};
1112

1213
export type TokenAudience = "graph" | "batch" | string | undefined;

‎package-lock.json

+1,964-100
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+9-4
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,14 @@
3434
},
3535
"license": "MIT",
3636
"devDependencies": {
37-
"@types/mocha": "^2.2.40",
38-
"@types/should": "^8.1.30",
37+
"@types/chai": "^4.1.4",
38+
"@types/mocha": "^2.2.48",
39+
"chai": "^4.1.2",
3940
"mocha": "^5.2.0",
40-
"should": "5.2.0",
41+
"nock": "^9.6.1",
42+
"npm-run-all": "^4.1.3",
43+
"nyc": "^13.0.1",
44+
"ts-node": "^7.0.1",
4145
"tslint": "^5.2.0",
4246
"typescript": "^2.5.2"
4347
},
@@ -51,7 +55,8 @@
5155
},
5256
"scripts": {
5357
"tsc": "tsc -p tsconfig.json",
54-
"test": "npm -s run-script tslint",
58+
"test": "run-p tslint test:unit",
59+
"test:unit": "nyc mocha",
5560
"unit": "mocha -t 50000 dist/test",
5661
"build": "npm -s run-script tsc",
5762
"tslint": "tslint -p . -c tslint.json --exclude test/**/*.ts"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
import * as msRestAzure from "../../lib/login";
5+
import { MSIAppServiceTokenCredentials } from "../../lib/credentials/msiAppServiceTokenCredentials";
6+
import { expect, assert } from "chai";
7+
import { WebResource, HttpHeaders, HttpClient, HttpOperationResponse } from "ms-rest-js";
8+
9+
describe("MSI App Service Authentication", function () {
10+
11+
function getMockHttpClient(response?: any, error?: any): HttpClient {
12+
const httpClient = {
13+
sendRequest: async (request: WebResource): Promise<HttpOperationResponse> => {
14+
if (error === undefined) {
15+
const httpResponse: HttpOperationResponse = {
16+
request: request,
17+
status: 200,
18+
headers: new HttpHeaders(),
19+
bodyAsText: JSON.stringify(response)
20+
};
21+
return Promise.resolve(httpResponse);
22+
} else {
23+
return Promise.reject(error);
24+
}
25+
}
26+
};
27+
28+
return httpClient;
29+
}
30+
31+
describe("Credential getToken()", async () => {
32+
it("should get token from the App service MSI by providing optional properties", async () => {
33+
const mockResponse = {
34+
access_token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1d",
35+
expires_in: "3599",
36+
expires_on: "1502930996",
37+
resource: "https://management.azure.com/",
38+
token_type: "Bearer"
39+
};
40+
41+
const httpClient = getMockHttpClient(mockResponse);
42+
const msiCredsObj = new MSIAppServiceTokenCredentials({
43+
msiEndpoint: "http://127.0.0.1:41741/MSI/token/",
44+
msiSecret: "69418689F1E342DD946CB82994CDA3CB",
45+
httpClient: httpClient
46+
});
47+
48+
const response = await msiCredsObj.getToken();
49+
expect(response).to.exist;
50+
expect(response!.accessToken).to.exist;
51+
expect(response!.tokenType).to.exist;
52+
});
53+
54+
it("should get token from the App service MSI by reading the environment variables", async () => {
55+
const mockResponse = {
56+
access_token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1d",
57+
expires_in: "3599",
58+
expires_on: "1502930996",
59+
resource: "https://management.azure.com/",
60+
token_type: "Bearer"
61+
};
62+
63+
64+
const httpClient = getMockHttpClient(mockResponse);
65+
process.env["MSI_ENDPOINT"] = "http://127.0.0.1:41741/MSI/token/";
66+
process.env["MSI_SECRET"] = "69418689F1E342DD946CB82994CDA3CB";
67+
const msiCredsObj = new MSIAppServiceTokenCredentials({ httpClient: httpClient });
68+
const response = await msiCredsObj.getToken();
69+
expect(response).to.exist;
70+
expect(response!.accessToken).to.exist;
71+
expect(response!.tokenType).to.exist;
72+
});
73+
74+
it('should throw if the response contains "ExceptionMessage"', async function () {
75+
const errorResponse = {
76+
"error": "unknown",
77+
"error_description": "ExceptionMessage: Failed to retrieve token from the Active directory. For details see logs in C:\\User1\\Logs\\Plugins\\Microsoft.Identity.MSI\\1.0\\service_identity_0.log"
78+
};
79+
80+
const httpClient = getMockHttpClient(undefined, errorResponse);
81+
process.env["MSI_ENDPOINT"] = "http://127.0.0.1:41741/MSI/token/";
82+
process.env["MSI_SECRET"] = "69418689F1E342DD946CB82994CDA3CB";
83+
const msiCredsObj = new MSIAppServiceTokenCredentials({ httpClient: httpClient });
84+
try {
85+
await msiCredsObj.getToken();
86+
assert.fail(undefined, undefined, "getToken should throw an exception");
87+
}
88+
catch (err) {
89+
expect(err);
90+
}
91+
});
92+
});
93+
94+
describe("loginWithAppServiceMSI (callback)", () => {
95+
96+
it("should successfully provide MSIAppServiceTokenCredentials object by providing optional properties", (done) => {
97+
const mockResponse = {
98+
access_token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1d",
99+
expires_in: "3599",
100+
expires_on: "1502930996",
101+
resource: "https://management.azure.com/",
102+
token_type: "Bearer"
103+
};
104+
105+
const httpClient = getMockHttpClient(mockResponse);
106+
107+
const options = {
108+
msiEndpoint: "http://127.0.0.1:41741/MSI/token/",
109+
msiSecret: "69418689F1E342DD946CB82994CDA3CB",
110+
httpClient: httpClient
111+
};
112+
113+
msRestAzure.loginWithAppServiceMSI(options, (error, response) => {
114+
expect(error).to.not.exist;
115+
expect(response).to.exist;
116+
expect(response instanceof MSIAppServiceTokenCredentials).to.be.true;
117+
done();
118+
});
119+
});
120+
121+
it("should successfully provide MSIAppServiceTokenCredentials object by reading the environment variables", async () => {
122+
const mockResponse = {
123+
access_token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1d",
124+
expires_in: "3599",
125+
expires_on: "1502930996",
126+
resource: "https://management.azure.com/",
127+
token_type: "Bearer"
128+
};
129+
130+
const httpClient = getMockHttpClient(mockResponse);
131+
process.env["MSI_ENDPOINT"] = "http://127.0.0.1:41741/MSI/token/";
132+
process.env["MSI_SECRET"] = "69418689F1E342DD946CB82994CDA3CB";
133+
134+
const response = await msRestAzure.loginWithAppServiceMSI({ httpClient: httpClient });
135+
136+
expect(response).to.exist;
137+
expect(response instanceof MSIAppServiceTokenCredentials).to.be.true;
138+
});
139+
140+
it('should throw if the response contains "ExceptionMessage"', async () => {
141+
const errorResponse = {
142+
"error": "unknown",
143+
"error_description": "ExceptionMessage: Failed to retrieve token from the Active directory. For details see logs in C:\\User1\\Logs\\Plugins\\Microsoft.Identity.MSI\\1.0\\service_identity_0.log"
144+
};
145+
146+
const httpClient = getMockHttpClient(undefined, errorResponse);
147+
process.env["MSI_ENDPOINT"] = "http://127.0.0.1:41741/MSI/token/";
148+
process.env["MSI_SECRET"] = "69418689F1E342DD946CB82994CDA3CB";
149+
150+
try {
151+
await msRestAzure.loginWithAppServiceMSI({ httpClient: httpClient });
152+
assert.fail(undefined, undefined, "loginWithAppServiceMSI should throw an exception");
153+
} catch (err) {
154+
expect(err).to.exist;
155+
}
156+
});
157+
});
158+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
import { MSIVmTokenCredentials } from "../../lib/credentials/msiVmTokenCredentials";
5+
import { expect, assert } from "chai";
6+
import { HttpClient, HttpOperationResponse, WebResource, HttpHeaders } from "ms-rest-js";
7+
8+
describe("MSI Vm Authentication", () => {
9+
10+
function setupNockResponse(port?: number, request?: any, response?: any, error?: any): HttpClient {
11+
if (!port) {
12+
port = 50342;
13+
}
14+
15+
const isMatch = (actualRequest: WebResource, expectedResource: any) => {
16+
return actualRequest.url === `http://localhost:${port}/oauth2/token` && actualRequest.body === `"resource=${encodeURIComponent(expectedResource.resource)}"`;
17+
};
18+
19+
const httpClient = {
20+
sendRequest: async (req: WebResource): Promise<HttpOperationResponse> => {
21+
if (error === undefined && isMatch(req, request)) {
22+
const httpResponse: HttpOperationResponse = {
23+
request: req,
24+
status: 200,
25+
headers: new HttpHeaders(),
26+
bodyAsText: JSON.stringify(response)
27+
};
28+
return Promise.resolve(httpResponse);
29+
} else {
30+
return Promise.reject(error);
31+
}
32+
}
33+
};
34+
35+
return httpClient;
36+
}
37+
38+
it("should get token from the virtual machine with MSI service running at default port", async () => {
39+
const mockResponse = {
40+
access_token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1d",
41+
refresh_token: "",
42+
expires_in: "3599",
43+
expires_on: "1502930996",
44+
not_before: "1502927096",
45+
resource: "https://management.azure.com/",
46+
token_type: "Bearer"
47+
};
48+
49+
const requestBodyToMatch = {
50+
"resource": "https://management.azure.com/"
51+
};
52+
53+
const httpClient = setupNockResponse(undefined, requestBodyToMatch, mockResponse);
54+
55+
const msiCredsObj = new MSIVmTokenCredentials({ httpClient: httpClient });
56+
const response = await msiCredsObj.getToken();
57+
expect(response).to.exist;
58+
expect(response!.accessToken).to.exist;
59+
expect(response!.tokenType).to.exist;
60+
});
61+
62+
it("should get token from the virtual machine with MSI service running at custom port", async () => {
63+
const mockResponse = {
64+
access_token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1d",
65+
refresh_token: "",
66+
expires_in: "3599",
67+
expires_on: "1502930996",
68+
not_before: "1502927096",
69+
resource: "https://management.azure.com/",
70+
token_type: "Bearer"
71+
};
72+
73+
const requestBodyToMatch = {
74+
"resource": "https://management.azure.com/"
75+
};
76+
77+
const customPort = 50341;
78+
const httpClient = setupNockResponse(customPort, requestBodyToMatch, mockResponse);
79+
80+
const msiCredsObj = new MSIVmTokenCredentials({ port: customPort, httpClient: httpClient });
81+
const response = await msiCredsObj.getToken();
82+
expect(response).to.exist;
83+
expect(response!.accessToken).to.exist;
84+
expect(response!.tokenType).to.exist;
85+
});
86+
87+
it("should throw on requests with bad resource", async () => {
88+
const errorMessage = "unknown";
89+
const errorDescription = "Failed to retrieve token from the Active directory. For details see logs in C:\\User1\\Logs\\Plugins\\Microsoft.Identity.MSI\\1.0\\service_identity_0.log";
90+
const errorResponse = {
91+
"error": errorMessage,
92+
"error_description": errorDescription
93+
};
94+
95+
const requestBodyToMatch = {
96+
"resource": "badvalue"
97+
};
98+
99+
const httpClient = setupNockResponse(undefined, requestBodyToMatch, undefined, errorResponse);
100+
const msiCredsObj = new MSIVmTokenCredentials({ "resource": "badvalue", httpClient: httpClient });
101+
102+
try {
103+
await msiCredsObj.getToken();
104+
assert.fail(undefined, undefined, "getToken should throw an exception");
105+
} catch (err) {
106+
expect(err).to.exist;
107+
expect((err as any).error).to.equal(errorMessage);
108+
expect((err as any).error_description).to.equal(errorDescription);
109+
}
110+
});
111+
112+
it("should throw on request with empty resource", async () => {
113+
const errorMessage = "bad_resource_200";
114+
const errorDescription = "Invalid Resource";
115+
const errorResponse = {
116+
"error": errorMessage,
117+
"error_description": errorDescription
118+
};
119+
120+
const requestBodyToMatch = {
121+
"resource": " "
122+
};
123+
124+
const httpClient = setupNockResponse(undefined, requestBodyToMatch, undefined, errorResponse);
125+
const msiCredsObj = new MSIVmTokenCredentials({ "resource": " ", httpClient: httpClient });
126+
127+
try {
128+
await msiCredsObj.getToken();
129+
assert.fail(undefined, undefined, "getToken should throw an exception");
130+
} catch (err) {
131+
expect(err).to.exist;
132+
expect((err as any).error).to.equal(errorMessage);
133+
expect((err as any).error_description).to.equal(errorDescription);
134+
}
135+
});
136+
});

‎test/mocha.opts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
--require ts-node/register
2+
--timeout 50000
3+
--reporter list
4+
--colors
5+
test/**/*.ts

‎tslint.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"prefer-const": true,
5151
"no-switch-case-fall-through": true,
5252
"triple-equals": true,
53-
"jsdoc-format": true
53+
"jsdoc-format": true,
54+
"max-classes-per-file": [true, 1]
5455
}
5556
}

0 commit comments

Comments
 (0)
This repository has been archived.