az2aws 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/login.js ADDED
@@ -0,0 +1,824 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.login = void 0;
7
+ const lodash_1 = __importDefault(require("lodash"));
8
+ const bluebird_1 = __importDefault(require("bluebird"));
9
+ const inquirer_1 = __importDefault(require("inquirer"));
10
+ const zlib_1 = __importDefault(require("zlib"));
11
+ const client_sts_1 = require("@aws-sdk/client-sts");
12
+ const cheerio_1 = require("cheerio");
13
+ const uuid_1 = require("uuid");
14
+ const puppeteer_1 = __importDefault(require("puppeteer"));
15
+ const querystring_1 = __importDefault(require("querystring"));
16
+ const debug_1 = __importDefault(require("debug"));
17
+ const CLIError_1 = require("./CLIError");
18
+ const awsConfig_1 = require("./awsConfig");
19
+ const proxy_agent_1 = __importDefault(require("proxy-agent"));
20
+ const paths_1 = require("./paths");
21
+ const mkdirp_1 = __importDefault(require("mkdirp"));
22
+ const https_1 = require("https");
23
+ const node_http_handler_1 = require("@smithy/node-http-handler");
24
+ const debug = (0, debug_1.default)("az2aws");
25
+ const WIDTH = 425;
26
+ const HEIGHT = 550;
27
+ const DELAY_ON_UNRECOGNIZED_PAGE = 1000;
28
+ const MAX_UNRECOGNIZED_PAGE_DELAY = 30 * 1000;
29
+ // source: https://docs.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-sso-quick-start#google-chrome-all-platforms
30
+ const AZURE_AD_SSO = "autologon.microsoftazuread-sso.com";
31
+ const AWS_SAML_ENDPOINT = "https://signin.aws.amazon.com/saml";
32
+ const AWS_CN_SAML_ENDPOINT = "https://signin.amazonaws.cn/saml";
33
+ const AWS_GOV_SAML_ENDPOINT = "https://signin.amazonaws-us-gov.com/saml";
34
+ /**
35
+ * To proxy the input/output of the Azure login page, it's easiest to run a loop that
36
+ * monitors the state of the page and then perform the corresponding CLI behavior.
37
+ * The states have a name that is used for the debug messages, a selector that is used
38
+ * with puppeteer's page.$(selector) to determine if the state is active, and a handler
39
+ * that is called if the state is active.
40
+ */
41
+ const states = [
42
+ {
43
+ name: "username input",
44
+ selector: `input[name="loginfmt"]:not(.moveOffScreen)`,
45
+ async handler(page, _selected, noPrompt, defaultUsername) {
46
+ const error = await page.$(".alert-error");
47
+ if (error) {
48
+ debug("Found error message. Displaying");
49
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
50
+ const errorMessage = await page.evaluate(
51
+ // eslint-disable-next-line
52
+ (err) => { var _a; return (_a = err === null || err === void 0 ? void 0 : err.textContent) !== null && _a !== void 0 ? _a : ""; }, error);
53
+ console.log(errorMessage);
54
+ }
55
+ let username;
56
+ if (noPrompt && defaultUsername) {
57
+ debug("Not prompting user for username");
58
+ username = defaultUsername;
59
+ }
60
+ else {
61
+ debug("Prompting user for username");
62
+ ({ username } = await inquirer_1.default.prompt([
63
+ {
64
+ name: "username",
65
+ message: "Username:",
66
+ default: defaultUsername,
67
+ },
68
+ ]));
69
+ }
70
+ debug("Waiting for username input to be visible");
71
+ await page.waitForSelector(`input[name="loginfmt"]`, {
72
+ visible: true,
73
+ timeout: 60000,
74
+ });
75
+ debug("Focusing on username input");
76
+ await page.focus(`input[name="loginfmt"]`);
77
+ debug("Clearing input");
78
+ for (let i = 0; i < 100; i++) {
79
+ await page.keyboard.press("Backspace");
80
+ }
81
+ debug("Typing username");
82
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
83
+ await page.keyboard.type(username);
84
+ await bluebird_1.default.delay(500);
85
+ debug("Waiting for submit button to be visible");
86
+ await page.waitForSelector(`input[type=submit]`, {
87
+ visible: true,
88
+ timeout: 60000,
89
+ });
90
+ debug("Submitting form");
91
+ await page.click("input[type=submit]");
92
+ await bluebird_1.default.delay(500);
93
+ debug("Waiting for submission to finish");
94
+ await Promise.race([
95
+ page.waitForSelector(`input[name=loginfmt].has-error,input[name=loginfmt].moveOffScreen`, { timeout: 60000 }),
96
+ (async () => {
97
+ await bluebird_1.default.delay(1000);
98
+ await page.waitForSelector(`input[name=loginfmt]`, {
99
+ hidden: true,
100
+ timeout: 60000,
101
+ });
102
+ })(),
103
+ ]);
104
+ },
105
+ },
106
+ {
107
+ name: "account selection",
108
+ selector: `#aadTile > div > div.table-cell.tile-img > img`,
109
+ async handler(page) {
110
+ debug("Multiple accounts associated with username.");
111
+ const aadTile = await page.$("#aadTileTitle");
112
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
113
+ const aadTileMessage = await page.evaluate(
114
+ // eslint-disable-next-line
115
+ (a) => { var _a; return (_a = a === null || a === void 0 ? void 0 : a.textContent) !== null && _a !== void 0 ? _a : ""; }, aadTile);
116
+ const msaTile = await page.$("#msaTileTitle");
117
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
118
+ const msaTileMessage = await page.evaluate(
119
+ // eslint-disable-next-line
120
+ (m) => { var _a; return (_a = m === null || m === void 0 ? void 0 : m.textContent) !== null && _a !== void 0 ? _a : ""; }, msaTile);
121
+ const accounts = [
122
+ { message: aadTileMessage, selector: "#aadTileTitle" },
123
+ { message: msaTileMessage, selector: "#msaTileTitle" },
124
+ ];
125
+ let account;
126
+ if (accounts.length === 0) {
127
+ throw new CLIError_1.CLIError("No accounts found on account selection screen.");
128
+ }
129
+ else if (accounts.length === 1) {
130
+ account = accounts[0];
131
+ }
132
+ else {
133
+ debug("Asking user to choose account");
134
+ console.log("It looks like this Username is used with more than one account from Microsoft. Which one do you want to use?");
135
+ const answers = await inquirer_1.default.prompt([
136
+ {
137
+ name: "account",
138
+ message: "Account:",
139
+ type: "list",
140
+ choices: lodash_1.default.map(accounts, "message"),
141
+ default: aadTileMessage,
142
+ },
143
+ ]);
144
+ account = lodash_1.default.find(accounts, ["message", answers.account]);
145
+ }
146
+ if (!account) {
147
+ throw new Error("Unable to find account");
148
+ }
149
+ debug(`Proceeding with account ${account.selector}`);
150
+ await page.click(account.selector);
151
+ await bluebird_1.default.delay(500);
152
+ },
153
+ },
154
+ {
155
+ name: "passwordless",
156
+ selector: `input[value='Send notification']`,
157
+ async handler(page) {
158
+ debug("Sending notification");
159
+ // eslint-disable-next-line
160
+ await page.click("input[value='Send notification']");
161
+ debug("Waiting for auth code");
162
+ // eslint-disable-next-line
163
+ await page.waitForSelector(`#idRemoteNGC_DisplaySign`, {
164
+ visible: true,
165
+ timeout: 60000,
166
+ });
167
+ debug("Printing the message displayed");
168
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
169
+ const messageElement = await page.$("#idDiv_RemoteNGC_PollingDescription");
170
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
171
+ const codeElement = await page.$("#idRemoteNGC_DisplaySign");
172
+ // eslint-disable-next-line
173
+ const message = await page.evaluate(
174
+ // eslint-disable-next-line
175
+ (el) => { var _a; return (_a = el === null || el === void 0 ? void 0 : el.textContent) !== null && _a !== void 0 ? _a : ""; }, messageElement);
176
+ console.log(message);
177
+ debug("Printing the auth code");
178
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
179
+ const authCode = await page.evaluate(
180
+ // eslint-disable-next-line
181
+ (el) => { var _a; return (_a = el === null || el === void 0 ? void 0 : el.textContent) !== null && _a !== void 0 ? _a : ""; }, codeElement);
182
+ console.log(authCode);
183
+ debug("Waiting for response");
184
+ await page.waitForSelector(`#idRemoteNGC_DisplaySign`, {
185
+ hidden: true,
186
+ timeout: 60000,
187
+ });
188
+ },
189
+ },
190
+ {
191
+ name: "password input",
192
+ selector: `input[name="Password"]:not(.moveOffScreen),input[name="passwd"]:not(.moveOffScreen)`,
193
+ async handler(page, _selected, noPrompt, _defaultUsername, defaultPassword) {
194
+ const error = await page.$(".alert-error");
195
+ if (error) {
196
+ debug("Found error message. Displaying");
197
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
198
+ const errorMessage = await page.evaluate(
199
+ // eslint-disable-next-line
200
+ (err) => { var _a; return (_a = err === null || err === void 0 ? void 0 : err.textContent) !== null && _a !== void 0 ? _a : ""; }, error);
201
+ console.log(errorMessage);
202
+ defaultPassword = ""; // Password error. Unset the default and allow user to enter it.
203
+ }
204
+ let password;
205
+ if (noPrompt && defaultPassword) {
206
+ debug("Not prompting user for password");
207
+ password = defaultPassword;
208
+ }
209
+ else {
210
+ debug("Prompting user for password");
211
+ ({ password } = await inquirer_1.default.prompt([
212
+ {
213
+ name: "password",
214
+ message: "Password:",
215
+ type: "password",
216
+ },
217
+ ]));
218
+ }
219
+ debug("Focusing on password input");
220
+ await page.focus(`input[name="Password"],input[name="passwd"]`);
221
+ debug("Typing password");
222
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
223
+ await page.keyboard.type(password);
224
+ debug("Submitting form");
225
+ await page.click("span[class=submit],input[type=submit]");
226
+ debug("Waiting for a delay");
227
+ await bluebird_1.default.delay(500);
228
+ },
229
+ },
230
+ {
231
+ name: "TFA instructions",
232
+ selector: `#idDiv_SAOTCAS_Description`,
233
+ async handler(page, selected) {
234
+ const descriptionMessage = await page.evaluate(
235
+ // eslint-disable-next-line
236
+ (description) => { var _a; return (_a = description === null || description === void 0 ? void 0 : description.textContent) !== null && _a !== void 0 ? _a : ""; }, selected);
237
+ console.log(descriptionMessage);
238
+ try {
239
+ debug("Checking if authentication code is displayed");
240
+ const authenticationCodeElement = await page.$("#idRichContext_DisplaySign");
241
+ debug("Reading the authentication code");
242
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
243
+ const authenticationCode = await page.evaluate(
244
+ // eslint-disable-next-line
245
+ (d) => { var _a; return (_a = d === null || d === void 0 ? void 0 : d.textContent) !== null && _a !== void 0 ? _a : ""; }, authenticationCodeElement);
246
+ debug("Printing the authentication code to console");
247
+ console.log(authenticationCode);
248
+ }
249
+ catch (_a) {
250
+ debug("No authentication code found on page");
251
+ }
252
+ debug("Waiting for response");
253
+ await page.waitForSelector(`#idDiv_SAOTCAS_Description`, {
254
+ hidden: true,
255
+ timeout: 60000,
256
+ });
257
+ },
258
+ },
259
+ {
260
+ name: "TFA failed",
261
+ selector: `#idDiv_SAASDS_Description,#idDiv_SAASTO_Description`,
262
+ async handler(page, selected) {
263
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
264
+ const descriptionMessage = await page.evaluate(
265
+ // eslint-disable-next-line
266
+ (description) => { var _a; return (_a = description === null || description === void 0 ? void 0 : description.textContent) !== null && _a !== void 0 ? _a : ""; }, selected);
267
+ throw new CLIError_1.CLIError(descriptionMessage);
268
+ },
269
+ },
270
+ {
271
+ name: "TFA code input",
272
+ selector: "input[name=otc]:not(.moveOffScreen)",
273
+ async handler(page) {
274
+ const error = await page.$(".alert-error");
275
+ if (error) {
276
+ debug("Found error message. Displaying");
277
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
278
+ const errorMessage = await page.evaluate(
279
+ // eslint-disable-next-line
280
+ (err) => { var _a; return (_a = err === null || err === void 0 ? void 0 : err.textContent) !== null && _a !== void 0 ? _a : ""; }, error);
281
+ console.log(errorMessage);
282
+ }
283
+ else {
284
+ const description = await page.$("#idDiv_SAOTCC_Description");
285
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
286
+ const descriptionMessage = await page.evaluate(
287
+ // eslint-disable-next-line
288
+ (d) => { var _a; return (_a = d === null || d === void 0 ? void 0 : d.textContent) !== null && _a !== void 0 ? _a : ""; }, description);
289
+ console.log(descriptionMessage);
290
+ }
291
+ const { verificationCode } = await inquirer_1.default.prompt([
292
+ {
293
+ name: "verificationCode",
294
+ message: "Verification Code:",
295
+ },
296
+ ]);
297
+ debug("Focusing on verification code input");
298
+ await page.focus(`input[name="otc"]`);
299
+ debug("Clearing input");
300
+ for (let i = 0; i < 100; i++) {
301
+ await page.keyboard.press("Backspace");
302
+ }
303
+ debug("Typing verification code");
304
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
305
+ await page.keyboard.type(verificationCode);
306
+ debug("Submitting form");
307
+ await page.click("input[type=submit]");
308
+ debug("Waiting for submission to finish");
309
+ await Promise.race([
310
+ page.waitForSelector(`input[name=otc].has-error,input[name=otc].moveOffScreen`, { timeout: 60000 }),
311
+ (async () => {
312
+ await bluebird_1.default.delay(1000);
313
+ await page.waitForSelector(`input[name=otc]`, {
314
+ hidden: true,
315
+ timeout: 60000,
316
+ });
317
+ })(),
318
+ ]);
319
+ },
320
+ },
321
+ {
322
+ name: "Remember me",
323
+ selector: `#KmsiDescription`,
324
+ async handler(page, _selected, _noPrompt, _defaultUsername, _defaultPassword, rememberMe) {
325
+ if (rememberMe) {
326
+ debug("Clicking remember me button");
327
+ await page.click("#idSIButton9");
328
+ }
329
+ else {
330
+ debug("Clicking don't remember button");
331
+ await page.click("#idBtn_Back");
332
+ }
333
+ debug("Waiting for a delay");
334
+ await bluebird_1.default.delay(500);
335
+ },
336
+ },
337
+ {
338
+ name: "Service exception",
339
+ selector: "#service_exception_message",
340
+ async handler(page, selected) {
341
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
342
+ const descriptionMessage = await page.evaluate(
343
+ // eslint-disable-next-line
344
+ (description) => { var _a; return (_a = description === null || description === void 0 ? void 0 : description.textContent) !== null && _a !== void 0 ? _a : ""; }, selected);
345
+ throw new CLIError_1.CLIError(descriptionMessage);
346
+ },
347
+ },
348
+ ];
349
+ exports.login = {
350
+ async loginAsync(profileName, mode, disableSandbox, noPrompt, enableChromeNetworkService, awsNoVerifySsl, enableChromeSeamlessSso, noDisableExtensions, disableGpu) {
351
+ let headless, cliProxy;
352
+ if (mode === "cli") {
353
+ headless = true;
354
+ cliProxy = true;
355
+ }
356
+ else if (mode === "gui") {
357
+ headless = false;
358
+ cliProxy = false;
359
+ }
360
+ else if (mode === "debug") {
361
+ headless = false;
362
+ cliProxy = true;
363
+ }
364
+ else {
365
+ throw new CLIError_1.CLIError("Invalid mode");
366
+ }
367
+ const profile = await this._loadProfileAsync(profileName);
368
+ let assertionConsumerServiceURL = AWS_SAML_ENDPOINT;
369
+ if (profile.region && profile.region.startsWith("us-gov")) {
370
+ assertionConsumerServiceURL = AWS_GOV_SAML_ENDPOINT;
371
+ }
372
+ if (profile.region && profile.region.startsWith("cn-")) {
373
+ assertionConsumerServiceURL = AWS_CN_SAML_ENDPOINT;
374
+ }
375
+ console.log("Using AWS SAML endpoint", assertionConsumerServiceURL);
376
+ const loginUrl = await this._createLoginUrlAsync(profile.azure_app_id_uri, profile.azure_tenant_id, assertionConsumerServiceURL);
377
+ const samlResponse = await this._performLoginAsync(loginUrl, headless, disableSandbox, cliProxy, noPrompt, enableChromeNetworkService, profile.azure_default_username, profile.azure_default_password, enableChromeSeamlessSso, profile.azure_default_remember_me, noDisableExtensions, disableGpu);
378
+ const roles = this._parseRolesFromSamlResponse(samlResponse);
379
+ const { role, durationHours } = await this._askUserForRoleAndDurationAsync(roles, noPrompt, profile.azure_default_role_arn, profile.azure_default_duration_hours);
380
+ await this._assumeRoleAsync(profileName, samlResponse, role, durationHours, awsNoVerifySsl, profile.region);
381
+ },
382
+ async loginAll(mode, disableSandbox, noPrompt, enableChromeNetworkService, awsNoVerifySsl, enableChromeSeamlessSso, forceRefresh, noDisableExtensions, disableGpu) {
383
+ const profiles = await awsConfig_1.awsConfig.getAllProfileNames();
384
+ if (!profiles) {
385
+ return;
386
+ }
387
+ for (const profile of profiles) {
388
+ debug(`Check if profile ${profile} is expired or is about to expire`);
389
+ if (!forceRefresh &&
390
+ !(await awsConfig_1.awsConfig.isProfileAboutToExpireAsync(profile))) {
391
+ debug(`Profile ${profile} not yet due for refresh.`);
392
+ continue;
393
+ }
394
+ debug(`Run login for profile: ${profile}`);
395
+ await this.loginAsync(profile, mode, disableSandbox, noPrompt, enableChromeNetworkService, awsNoVerifySsl, enableChromeSeamlessSso, noDisableExtensions, disableGpu);
396
+ }
397
+ },
398
+ // Gather data from environment variables
399
+ _loadProfileFromEnv() {
400
+ const env = {};
401
+ const options = [
402
+ "azure_tenant_id",
403
+ "azure_app_id_uri",
404
+ "azure_default_username",
405
+ "azure_default_password",
406
+ "azure_default_role_arn",
407
+ "azure_default_duration_hours",
408
+ ];
409
+ for (let i = 0; i < options.length; i++) {
410
+ const opt = options[i];
411
+ const envVar = process.env[opt];
412
+ const envVarUpperCase = process.env[opt.toUpperCase()];
413
+ if (envVar) {
414
+ env[opt] = envVar;
415
+ }
416
+ else if (envVarUpperCase) {
417
+ env[opt] = envVarUpperCase;
418
+ }
419
+ }
420
+ debug("Environment");
421
+ debug({
422
+ ...env,
423
+ azure_default_password: "xxxxxxxxxx",
424
+ });
425
+ return env;
426
+ },
427
+ // Load the profile
428
+ async _loadProfileAsync(profileName) {
429
+ const profile = await awsConfig_1.awsConfig.getProfileConfigAsync(profileName);
430
+ if (!profile)
431
+ throw new CLIError_1.CLIError(`Unknown profile '${profileName}'. You must configure it first with --configure.`);
432
+ const env = this._loadProfileFromEnv();
433
+ for (const prop in env) {
434
+ if (env[prop]) {
435
+ profile[prop] = env[prop] === null ? profile[prop] : env[prop];
436
+ }
437
+ }
438
+ if (!profile.azure_tenant_id || !profile.azure_app_id_uri)
439
+ throw new CLIError_1.CLIError(`Profile '${profileName}' is not configured properly.`);
440
+ console.log(`Logging in with profile '${profileName}'...`);
441
+ return profile;
442
+ },
443
+ /**
444
+ * Create the Azure login SAML URL.
445
+ * @param {string} appIdUri - The app ID URI
446
+ * @param {string} tenantId - The Azure tenant ID
447
+ * @param {string} assertionConsumerServiceURL - The AWS SAML endpoint that Azure should send the SAML response to
448
+ * @returns {string} The login URL
449
+ * @private
450
+ */
451
+ _createLoginUrlAsync(appIdUri, tenantId, assertionConsumerServiceURL) {
452
+ debug("Generating UUID for SAML request");
453
+ const id = (0, uuid_1.v4)();
454
+ const samlRequest = `
455
+ <samlp:AuthnRequest xmlns="urn:oasis:names:tc:SAML:2.0:metadata" ID="id${id}" Version="2.0" IssueInstant="${new Date().toISOString()}" IsPassive="false" AssertionConsumerServiceURL="${assertionConsumerServiceURL}" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
456
+ <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">${appIdUri}</Issuer>
457
+ <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"></samlp:NameIDPolicy>
458
+ </samlp:AuthnRequest>
459
+ `;
460
+ debug("Generated SAML request", samlRequest);
461
+ debug("Deflating SAML");
462
+ return new Promise((resolve, reject) => {
463
+ zlib_1.default.deflateRaw(samlRequest, (err, samlBuffer) => {
464
+ if (err) {
465
+ return reject(err);
466
+ }
467
+ debug("Encoding SAML in base64");
468
+ const samlBase64 = samlBuffer.toString("base64");
469
+ const url = `https://login.microsoftonline.com/${tenantId}/saml2?SAMLRequest=${encodeURIComponent(samlBase64)}`;
470
+ debug("Created login URL", url);
471
+ return resolve(url);
472
+ });
473
+ });
474
+ },
475
+ /**
476
+ * Perform the login using Chrome.
477
+ * @param {string} url - The login URL
478
+ * @param {boolean} headless - True to hide the GUI, false to show it.
479
+ * @param {boolean} disableSandbox - True to disable the Puppeteer sandbox.
480
+ * @param {boolean} cliProxy - True to proxy input/output through the CLI, false to leave it in the GUI
481
+ * @param {bool} [noPrompt] - Enable skipping of user prompting
482
+ * @param {bool} [enableChromeNetworkService] - Enable chrome network service.
483
+ * @param {string} [defaultUsername] - The default username
484
+ * @param {string} [defaultPassword] - The default password
485
+ * @param {bool} [enableChromeSeamlessSso] - chrome seamless SSO
486
+ * @param {bool} [rememberMe] - Enable remembering the session
487
+ * @param {bool} [noDisableExtensions] - True to prevent Puppeteer from disabling Chromium extensions
488
+ * @param {bool} [disableGpu] - Disables GPU Acceleration
489
+ * @returns {Promise.<string>} The SAML response.
490
+ * @private
491
+ */
492
+ async _performLoginAsync(url, headless, disableSandbox, cliProxy, noPrompt, enableChromeNetworkService, defaultUsername, defaultPassword, enableChromeSeamlessSso, rememberMe, noDisableExtensions, disableGpu) {
493
+ debug("Loading login page in Chrome");
494
+ let browser;
495
+ try {
496
+ const args = headless
497
+ ? []
498
+ : [`--app=${url}`, `--window-size=${WIDTH},${HEIGHT}`];
499
+ if (disableSandbox)
500
+ args.push("--no-sandbox");
501
+ if (enableChromeNetworkService)
502
+ args.push("--enable-features=NetworkService");
503
+ if (enableChromeSeamlessSso)
504
+ args.push(`--auth-server-whitelist=${AZURE_AD_SSO}`, `--auth-negotiate-delegate-whitelist=${AZURE_AD_SSO}`);
505
+ debug(`rememberMe value: ${rememberMe} (type: ${typeof rememberMe})`);
506
+ if (rememberMe) {
507
+ if (paths_1.paths.userDataDir) {
508
+ args.push(`--user-data-dir=${paths_1.paths.userDataDir}`);
509
+ }
510
+ else {
511
+ await (0, mkdirp_1.default)(paths_1.paths.chromium);
512
+ args.push(`--user-data-dir=${paths_1.paths.chromium}`);
513
+ }
514
+ // --profile-directory requires --user-data-dir to work properly
515
+ if (paths_1.paths.profileDir) {
516
+ args.push(`--profile-directory=${paths_1.paths.profileDir}`);
517
+ }
518
+ }
519
+ if (process.env.https_proxy) {
520
+ args.push(`--proxy-server=${process.env.https_proxy}`);
521
+ }
522
+ const ignoreDefaultArgs = noDisableExtensions
523
+ ? ["--disable-extensions"]
524
+ : [];
525
+ if (disableGpu) {
526
+ args.push("--disable-gpu");
527
+ }
528
+ const launchParams = {
529
+ headless,
530
+ args,
531
+ ignoreDefaultArgs,
532
+ };
533
+ if (paths_1.paths.chromeBin) {
534
+ launchParams.executablePath = paths_1.paths.chromeBin;
535
+ }
536
+ browser = await puppeteer_1.default.launch(launchParams);
537
+ // Wait for a bit as sometimes the browser isn't ready.
538
+ await bluebird_1.default.delay(200);
539
+ const pages = await browser.pages();
540
+ const page = pages[0];
541
+ await page.setExtraHTTPHeaders({
542
+ "Accept-Language": "en",
543
+ });
544
+ await page.setViewport({ width: WIDTH - 15, height: HEIGHT - 35 });
545
+ // Prevent redirection to AWS
546
+ let samlResponseData;
547
+ const samlResponsePromise = new Promise((resolve) => {
548
+ page.on("request", (req) => {
549
+ const reqURL = req.url();
550
+ debug(`Request: ${url}`);
551
+ if (reqURL === AWS_SAML_ENDPOINT ||
552
+ reqURL === AWS_GOV_SAML_ENDPOINT ||
553
+ reqURL === AWS_CN_SAML_ENDPOINT) {
554
+ resolve(undefined);
555
+ samlResponseData = req.postData();
556
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
557
+ req.respond({
558
+ status: 200,
559
+ contentType: "text/plain",
560
+ headers: {},
561
+ body: "",
562
+ });
563
+ if (browser) {
564
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
565
+ browser.close();
566
+ }
567
+ browser = undefined;
568
+ debug(`Received SAML response, browser closed`);
569
+ }
570
+ else {
571
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
572
+ req.continue();
573
+ }
574
+ });
575
+ });
576
+ debug("Enabling request interception");
577
+ await page.setRequestInterception(true);
578
+ try {
579
+ if (headless || (!headless && cliProxy)) {
580
+ debug("Going to login page");
581
+ await page.goto(url, { waitUntil: "domcontentloaded" });
582
+ }
583
+ else {
584
+ debug("Waiting for login page to load");
585
+ await page.waitForNavigation({ waitUntil: "networkidle0" });
586
+ }
587
+ }
588
+ catch (err) {
589
+ if (err instanceof Error) {
590
+ // An error will be thrown if you're still logged in cause the page.goto ot waitForNavigation
591
+ // will be a redirect to AWS. That's usually OK
592
+ debug(`Error occured during loading the first page: ${err.message}`);
593
+ }
594
+ }
595
+ if (cliProxy) {
596
+ let totalUnrecognizedDelay = 0;
597
+ // eslint-disable-next-line no-constant-condition
598
+ while (true) {
599
+ if (samlResponseData)
600
+ break;
601
+ let foundState = false;
602
+ for (let i = 0; i < states.length; i++) {
603
+ const state = states[i];
604
+ let selected;
605
+ try {
606
+ selected = await page.$(state.selector);
607
+ }
608
+ catch (err) {
609
+ if (err instanceof Error) {
610
+ // An error can be thrown if the page isn't in a good state.
611
+ // If one occurs, try again after another loop.
612
+ debug(`Error when running state "${state.name}". ${err.toString()}. Retrying...`);
613
+ }
614
+ break;
615
+ }
616
+ if (selected) {
617
+ foundState = true;
618
+ debug(`Found state: ${state.name}`);
619
+ await Promise.race([
620
+ samlResponsePromise,
621
+ state.handler(page, selected, noPrompt, defaultUsername, defaultPassword, rememberMe),
622
+ ]);
623
+ debug(`Finished state: ${state.name}`);
624
+ break;
625
+ }
626
+ }
627
+ if (foundState) {
628
+ totalUnrecognizedDelay = 0;
629
+ }
630
+ else {
631
+ debug("State not recognized!");
632
+ if (totalUnrecognizedDelay > MAX_UNRECOGNIZED_PAGE_DELAY) {
633
+ const path = "az2aws-unrecognized-state.png";
634
+ await page.screenshot({ path });
635
+ throw new CLIError_1.CLIError(`Unable to recognize page state! A screenshot has been dumped to ${path}. If this problem persists, try running with --mode=gui or --mode=debug`);
636
+ }
637
+ totalUnrecognizedDelay += DELAY_ON_UNRECOGNIZED_PAGE;
638
+ await bluebird_1.default.delay(DELAY_ON_UNRECOGNIZED_PAGE);
639
+ }
640
+ }
641
+ }
642
+ else {
643
+ console.log("Please complete the login in the opened window");
644
+ await samlResponsePromise;
645
+ }
646
+ if (!samlResponseData) {
647
+ throw new Error("SAML response not found");
648
+ }
649
+ const samlResponse = querystring_1.default.parse(samlResponseData).SAMLResponse;
650
+ debug("Found SAML response", samlResponse);
651
+ if (!samlResponse) {
652
+ throw new Error("SAML response not found");
653
+ }
654
+ else if (Array.isArray(samlResponse)) {
655
+ throw new Error("SAML can't be an array");
656
+ }
657
+ return samlResponse;
658
+ }
659
+ finally {
660
+ if (browser) {
661
+ await browser.close();
662
+ }
663
+ }
664
+ },
665
+ /**
666
+ * Parse AWS roles out of the SAML response
667
+ * @param {string} assertion - The SAML assertion
668
+ * @returns {Array.<{roleArn: string, principalArn: string}>} The roles
669
+ * @private
670
+ */
671
+ _parseRolesFromSamlResponse(assertion) {
672
+ debug("Converting assertion from base64 to ASCII");
673
+ const samlText = Buffer.from(assertion, "base64").toString("ascii");
674
+ debug("Converted", samlText);
675
+ debug("Parsing SAML XML");
676
+ const saml = (0, cheerio_1.load)(samlText, { xmlMode: true });
677
+ debug("Looking for role SAML attribute");
678
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
679
+ const roles = saml("Attribute[Name='https://aws.amazon.com/SAML/Attributes/Role']>AttributeValue")
680
+ .map(function () {
681
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
682
+ // @ts-ignore
683
+ const roleAndPrincipal = saml(this).text();
684
+ const parts = roleAndPrincipal.split(",");
685
+ // Role / Principal claims may be in either order
686
+ const [roleIdx, principalIdx] = parts[0].includes(":role/")
687
+ ? [0, 1]
688
+ : [1, 0];
689
+ const roleArn = parts[roleIdx].trim();
690
+ const principalArn = parts[principalIdx].trim();
691
+ return { roleArn, principalArn };
692
+ })
693
+ .get();
694
+ debug("Found roles", roles);
695
+ return roles;
696
+ },
697
+ /**
698
+ * Ask the user for the role they want to use.
699
+ * @param {Array.<{roleArn: string, principalArn: string}>} roles - The roles to pick from
700
+ * @param {bool} [noPrompt] - Enable skipping of user prompting
701
+ * @param {string} [defaultRoleArn] - The default role ARN
702
+ * @param {number} [defaultDurationHours] - The default session duration in hours
703
+ * @returns {Promise.<{role: string, durationHours: number}>} The selected role and duration
704
+ * @private
705
+ */
706
+ async _askUserForRoleAndDurationAsync(roles, noPrompt, defaultRoleArn, defaultDurationHours) {
707
+ let role;
708
+ let durationHours = parseInt(defaultDurationHours, 10);
709
+ const questions = [];
710
+ if (roles.length === 0) {
711
+ throw new CLIError_1.CLIError("No roles found in SAML response.");
712
+ }
713
+ else if (roles.length === 1) {
714
+ debug("Choosing the only role in response");
715
+ role = roles[0];
716
+ }
717
+ else {
718
+ if (noPrompt && defaultRoleArn) {
719
+ role = lodash_1.default.find(roles, ["roleArn", defaultRoleArn]);
720
+ }
721
+ if (role) {
722
+ debug("Valid role found. No need to ask.");
723
+ }
724
+ else {
725
+ debug("Asking user to choose role");
726
+ questions.push({
727
+ name: "role",
728
+ message: "Role:",
729
+ type: "list",
730
+ choices: lodash_1.default.sortBy(lodash_1.default.map(roles, "roleArn")),
731
+ default: defaultRoleArn,
732
+ });
733
+ }
734
+ }
735
+ if (noPrompt && defaultDurationHours) {
736
+ debug("Default durationHours found. No need to ask.");
737
+ }
738
+ else {
739
+ questions.push({
740
+ name: "durationHours",
741
+ message: "Session Duration Hours (up to 12):",
742
+ type: "input",
743
+ default: defaultDurationHours || 1,
744
+ validate: (input) => {
745
+ input = Number(input);
746
+ if (input > 0 && input <= 12)
747
+ return true;
748
+ return "Duration hours must be between 0 and 12";
749
+ },
750
+ });
751
+ }
752
+ // Don't prompt for questions if not needed, an unneeded TTYWRAP prevents node from exiting when
753
+ // user is logged in and using multiple profiles --all-profiles and --no-prompt
754
+ if (questions.length > 0) {
755
+ const answers = await inquirer_1.default.prompt(questions);
756
+ if (!role)
757
+ role = lodash_1.default.find(roles, ["roleArn", answers.role]);
758
+ if (answers.durationHours) {
759
+ durationHours = parseInt(answers.durationHours, 10);
760
+ }
761
+ }
762
+ if (!role) {
763
+ throw new Error(`Unable to find role`);
764
+ }
765
+ return { role, durationHours };
766
+ },
767
+ /**
768
+ * Assume the role.
769
+ * @param {string} profileName - The profile name
770
+ * @param {string} assertion - The SAML assertion
771
+ * @param {string} role - The role to assume
772
+ * @param {number} durationHours - The session duration in hours
773
+ * @param {bool} awsNoVerifySsl - Whether to have the AWS CLI verify SSL
774
+ * @param {string} region - AWS region, if specified
775
+ * @returns {Promise} A promise
776
+ * @private
777
+ */
778
+ async _assumeRoleAsync(profileName, assertion, role, durationHours, awsNoVerifySsl, region) {
779
+ var _a, _b, _c, _d, _e;
780
+ console.log(`Assuming role ${role.roleArn} in region ${region}...`);
781
+ let stsOptions = {};
782
+ if (process.env.https_proxy) {
783
+ stsOptions = {
784
+ ...stsOptions,
785
+ requestHandler: new node_http_handler_1.NodeHttpHandler({
786
+ httpsAgent: (0, proxy_agent_1.default)(process.env.https_proxy),
787
+ }),
788
+ };
789
+ }
790
+ if (awsNoVerifySsl) {
791
+ stsOptions = {
792
+ ...stsOptions,
793
+ requestHandler: new node_http_handler_1.NodeHttpHandler({
794
+ httpsAgent: new https_1.Agent({
795
+ rejectUnauthorized: false,
796
+ }),
797
+ }),
798
+ };
799
+ }
800
+ if (region) {
801
+ stsOptions = {
802
+ ...stsOptions,
803
+ region,
804
+ };
805
+ }
806
+ const sts = new client_sts_1.STS(stsOptions);
807
+ const res = await sts.assumeRoleWithSAML({
808
+ PrincipalArn: role.principalArn,
809
+ RoleArn: role.roleArn,
810
+ SAMLAssertion: assertion,
811
+ DurationSeconds: Math.round(durationHours * 60 * 60),
812
+ });
813
+ if (!res.Credentials) {
814
+ debug("Unable to get security credentials from AWS");
815
+ return;
816
+ }
817
+ await awsConfig_1.awsConfig.setProfileCredentialsAsync(profileName, {
818
+ aws_access_key_id: (_a = res.Credentials.AccessKeyId) !== null && _a !== void 0 ? _a : "",
819
+ aws_secret_access_key: (_b = res.Credentials.SecretAccessKey) !== null && _b !== void 0 ? _b : "",
820
+ aws_session_token: (_c = res.Credentials.SessionToken) !== null && _c !== void 0 ? _c : "",
821
+ aws_expiration: (_e = (_d = res.Credentials.Expiration) === null || _d === void 0 ? void 0 : _d.toISOString()) !== null && _e !== void 0 ? _e : "",
822
+ });
823
+ },
824
+ };