az2aws 1.6.1 → 1.7.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 CHANGED
@@ -4,23 +4,24 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.login = void 0;
7
- const bluebird_1 = __importDefault(require("bluebird"));
7
+ const promises_1 = require("node:timers/promises");
8
+ const node_crypto_1 = __importDefault(require("node:crypto"));
8
9
  const inquirer_1 = __importDefault(require("inquirer"));
9
10
  const zlib_1 = __importDefault(require("zlib"));
10
11
  const client_sts_1 = require("@aws-sdk/client-sts");
11
12
  const cheerio_1 = require("cheerio");
12
- const uuid_1 = require("uuid");
13
13
  const puppeteer_1 = __importDefault(require("puppeteer"));
14
14
  const querystring_1 = __importDefault(require("querystring"));
15
15
  const debug_1 = __importDefault(require("debug"));
16
16
  const CLIError_1 = require("./CLIError");
17
17
  const awsConfig_1 = require("./awsConfig");
18
18
  const paths_1 = require("./paths");
19
- const mkdirp_1 = require("mkdirp");
20
- const promises_1 = __importDefault(require("fs/promises"));
19
+ const promises_2 = __importDefault(require("fs/promises"));
21
20
  const https_1 = require("https");
22
21
  const node_http_handler_1 = require("@smithy/node-http-handler");
23
22
  const loginStates_1 = require("./loginStates");
23
+ const sessionDuration_1 = require("./sessionDuration");
24
+ const sensitiveOutput_1 = require("./sensitiveOutput");
24
25
  const debug = (0, debug_1.default)("az2aws");
25
26
  const WIDTH = 425;
26
27
  const HEIGHT = 550;
@@ -31,6 +32,7 @@ const AZURE_AD_SSO = "autologon.microsoftazuread-sso.com";
31
32
  const AWS_SAML_ENDPOINT = "https://signin.aws.amazon.com/saml";
32
33
  const AWS_CN_SAML_ENDPOINT = "https://signin.amazonaws.cn/saml";
33
34
  const AWS_GOV_SAML_ENDPOINT = "https://signin.amazonaws-us-gov.com/saml";
35
+ const REDACTED = "[redacted]";
34
36
  // Keep the runtime import as native `import()` so CommonJS output can load
35
37
  // the ESM-only https-proxy-agent package.
36
38
  // eslint-disable-next-line @typescript-eslint/no-implied-eval
@@ -39,48 +41,77 @@ const getProxyUrl = () => process.env.https_proxy ||
39
41
  process.env.HTTPS_PROXY ||
40
42
  process.env.http_proxy ||
41
43
  process.env.HTTP_PROXY;
44
+ function handleBackgroundPromise(promise, description) {
45
+ void promise.catch((error) => {
46
+ const message = (0, sensitiveOutput_1.formatDebugErrorMessage)(error);
47
+ debug(`${description}: ${message}`);
48
+ });
49
+ }
42
50
  exports.login = {
43
51
  async _createHttpsProxyAgentAsync(proxyUrl, proxyOptions) {
44
52
  const { HttpsProxyAgent } = await importHttpsProxyAgent();
45
53
  return new HttpsProxyAgent(proxyUrl, proxyOptions);
46
54
  },
47
- async loginAsync(profileName, mode, disableSandbox, noPrompt, enableChromeNetworkService, awsNoVerifySsl, enableChromeSeamlessSso, noDisableExtensions, disableGpu, incognito = false) {
48
- let headless, cliProxy;
49
- if (mode === "cli") {
50
- headless = true;
51
- cliProxy = true;
52
- }
53
- else if (mode === "gui") {
54
- headless = false;
55
- cliProxy = false;
56
- }
57
- else if (mode === "debug") {
58
- headless = false;
59
- cliProxy = true;
60
- }
61
- else {
62
- throw new CLIError_1.CLIError("Invalid mode");
63
- }
64
- const profile = await this._loadProfileAsync(profileName);
65
- console.log(`Using AWS region ${profile.region || "(from AWS SDK defaults)"}`);
66
- if (profile.region && profile.region.startsWith("us-gov")) {
67
- console.warn("GovCloud region detected in profile. Note: Other AWS CLI operations " +
68
- "will use your AWS CLI default region. If needed, set it to match " +
69
- "this GovCloud region (us-gov-west-1 or us-gov-east-1).");
70
- }
71
- let assertionConsumerServiceURL = AWS_SAML_ENDPOINT;
72
- if (profile.region && profile.region.startsWith("us-gov")) {
73
- assertionConsumerServiceURL = AWS_GOV_SAML_ENDPOINT;
55
+ async loginAsync(profileName, mode, disableSandbox, noPrompt, enableChromeNetworkService, awsNoVerifySsl, enableChromeSeamlessSso, noDisableExtensions, disableGpu, incognito = false, credentialProcess = false) {
56
+ const originalConsoleLog = console.log;
57
+ const effectiveNoPrompt = credentialProcess ? true : noPrompt;
58
+ try {
59
+ if (credentialProcess) {
60
+ console.log = (...args) => console.error(...args);
61
+ }
62
+ let headless, cliProxy;
63
+ if (mode === "cli") {
64
+ headless = true;
65
+ cliProxy = true;
66
+ }
67
+ else if (mode === "gui") {
68
+ headless = false;
69
+ cliProxy = false;
70
+ }
71
+ else if (mode === "debug") {
72
+ headless = false;
73
+ cliProxy = true;
74
+ }
75
+ else {
76
+ throw new CLIError_1.CLIError("Invalid mode");
77
+ }
78
+ const profile = await this._loadProfileAsync(profileName);
79
+ console.log(`Using AWS region ${profile.region || "(from AWS SDK defaults)"}`);
80
+ if (profile.region && profile.region.startsWith("us-gov")) {
81
+ console.warn("GovCloud region detected in profile. Note: Other AWS CLI operations " +
82
+ "will use your AWS CLI default region. If needed, set it to match " +
83
+ "this GovCloud region (us-gov-west-1 or us-gov-east-1).");
84
+ }
85
+ let assertionConsumerServiceURL = AWS_SAML_ENDPOINT;
86
+ if (profile.region && profile.region.startsWith("us-gov")) {
87
+ assertionConsumerServiceURL = AWS_GOV_SAML_ENDPOINT;
88
+ }
89
+ if (profile.region && profile.region.startsWith("cn-")) {
90
+ assertionConsumerServiceURL = AWS_CN_SAML_ENDPOINT;
91
+ }
92
+ console.log("Using AWS SAML endpoint", assertionConsumerServiceURL);
93
+ const loginUrl = await this._createLoginUrlAsync(profile.azure_app_id_uri, profile.azure_tenant_id, assertionConsumerServiceURL);
94
+ const allowSensitiveOutput = (0, sensitiveOutput_1.shouldAllowSensitiveOutput)();
95
+ const samlResponse = await this._performLoginAsync(loginUrl, headless, disableSandbox, cliProxy, effectiveNoPrompt, enableChromeNetworkService, profile.azure_default_username, profile.azure_default_password, enableChromeSeamlessSso, profile.azure_default_remember_me, noDisableExtensions, disableGpu, incognito, allowSensitiveOutput);
96
+ const roles = this._parseRolesFromSamlResponse(samlResponse);
97
+ const { role, durationHours } = await this._askUserForRoleAndDurationAsync(roles, effectiveNoPrompt, profile.azure_default_role_arn, profile.azure_default_duration_hours, credentialProcess ? "--credential-process" : "--no-prompt");
98
+ const credentials = await this._assumeRoleAsync(profileName, samlResponse, role, durationHours, awsNoVerifySsl, profile.region, !credentialProcess);
99
+ if (credentialProcess) {
100
+ if (!credentials) {
101
+ throw new CLIError_1.CLIError("Unable to retrieve credentials.");
102
+ }
103
+ originalConsoleLog(JSON.stringify({
104
+ Version: 1,
105
+ AccessKeyId: credentials.aws_access_key_id,
106
+ SecretAccessKey: credentials.aws_secret_access_key,
107
+ SessionToken: credentials.aws_session_token,
108
+ Expiration: credentials.aws_expiration,
109
+ }));
110
+ }
74
111
  }
75
- if (profile.region && profile.region.startsWith("cn-")) {
76
- assertionConsumerServiceURL = AWS_CN_SAML_ENDPOINT;
112
+ finally {
113
+ console.log = originalConsoleLog;
77
114
  }
78
- console.log("Using AWS SAML endpoint", assertionConsumerServiceURL);
79
- const loginUrl = await this._createLoginUrlAsync(profile.azure_app_id_uri, profile.azure_tenant_id, assertionConsumerServiceURL);
80
- 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, incognito);
81
- const roles = this._parseRolesFromSamlResponse(samlResponse);
82
- const { role, durationHours } = await this._askUserForRoleAndDurationAsync(roles, noPrompt, profile.azure_default_role_arn, profile.azure_default_duration_hours);
83
- await this._assumeRoleAsync(profileName, samlResponse, role, durationHours, awsNoVerifySsl, profile.region);
84
115
  },
85
116
  async loginAll(mode, disableSandbox, noPrompt, enableChromeNetworkService, awsNoVerifySsl, enableChromeSeamlessSso, forceRefresh, noDisableExtensions, disableGpu, incognito = false) {
86
117
  const profiles = await awsConfig_1.awsConfig.getAllProfileNames();
@@ -118,12 +149,22 @@ exports.login = {
118
149
  }
119
150
  }
120
151
  debug("Environment");
121
- debug({
122
- ...env,
123
- azure_default_password: "xxxxxxxxxx",
124
- });
152
+ debug(this._redactProfileForDebug(env));
125
153
  return env;
126
154
  },
155
+ _redactProfileForDebug(env) {
156
+ return Object.fromEntries(Object.entries(env).map(([key, value]) => [
157
+ key,
158
+ key === "azure_default_duration_hours" ? value : REDACTED,
159
+ ]));
160
+ },
161
+ _redactArnForDebug(arn) {
162
+ const match = arn.match(/^(arn:[^:]+:iam::)[^:]+:(.+?\/).+$/);
163
+ if (!match) {
164
+ return arn;
165
+ }
166
+ return `${match[1]}${REDACTED}:${match[2]}${REDACTED}`;
167
+ },
127
168
  // Load the profile
128
169
  async _loadProfileAsync(profileName) {
129
170
  const profile = await awsConfig_1.awsConfig.getProfileConfigAsync(profileName);
@@ -150,14 +191,14 @@ exports.login = {
150
191
  */
151
192
  _createLoginUrlAsync(appIdUri, tenantId, assertionConsumerServiceURL) {
152
193
  debug("Generating UUID for SAML request");
153
- const id = (0, uuid_1.v4)();
194
+ const id = node_crypto_1.default.randomUUID();
154
195
  const samlRequest = `
155
196
  <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">
156
197
  <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">${appIdUri}</Issuer>
157
198
  <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"></samlp:NameIDPolicy>
158
199
  </samlp:AuthnRequest>
159
200
  `;
160
- debug("Generated SAML request", samlRequest);
201
+ debug("Generated SAML request");
161
202
  debug("Deflating SAML");
162
203
  return new Promise((resolve, reject) => {
163
204
  zlib_1.default.deflateRaw(samlRequest, (err, samlBuffer) => {
@@ -167,7 +208,7 @@ exports.login = {
167
208
  debug("Encoding SAML in base64");
168
209
  const samlBase64 = samlBuffer.toString("base64");
169
210
  const url = `https://login.microsoftonline.com/${tenantId}/saml2?SAMLRequest=${encodeURIComponent(samlBase64)}`;
170
- debug("Created login URL", url);
211
+ debug("Created login URL", (0, sensitiveOutput_1.redactUrlForLogs)(url));
171
212
  return resolve(url);
172
213
  });
173
214
  });
@@ -190,7 +231,7 @@ exports.login = {
190
231
  * @returns {Promise.<string>} The SAML response.
191
232
  * @private
192
233
  */
193
- async _performLoginAsync(url, headless, disableSandbox, cliProxy, noPrompt, enableChromeNetworkService, defaultUsername, defaultPassword, enableChromeSeamlessSso, rememberMe, noDisableExtensions, disableGpu, incognito = false) {
234
+ async _performLoginAsync(url, headless, disableSandbox, cliProxy, noPrompt, enableChromeNetworkService, defaultUsername, defaultPassword, enableChromeSeamlessSso, rememberMe, noDisableExtensions, disableGpu, incognito = false, allowSensitiveStateOutput = true) {
194
235
  debug("Loading login page in Chrome");
195
236
  let browser;
196
237
  const useRememberMe = rememberMe && !incognito;
@@ -212,7 +253,7 @@ exports.login = {
212
253
  args.push(`--user-data-dir=${paths_1.paths.userDataDir}`);
213
254
  }
214
255
  else {
215
- await (0, mkdirp_1.mkdirp)(paths_1.paths.chromium);
256
+ await promises_2.default.mkdir(paths_1.paths.chromium, { recursive: true });
216
257
  args.push(`--user-data-dir=${paths_1.paths.chromium}`);
217
258
  }
218
259
  // --profile-directory requires --user-data-dir to work properly
@@ -249,10 +290,10 @@ exports.login = {
249
290
  e.name === "TargetCloseError" &&
250
291
  useRememberMe &&
251
292
  !paths_1.paths.userDataDir) {
252
- debug(`Browser launch failed with TargetCloseError. Resetting profile at ${paths_1.paths.chromium}`);
293
+ debug("Browser launch failed with TargetCloseError. Resetting managed browser profile.");
253
294
  console.warn("Browser profile appears incompatible. Resetting profile data and retrying...");
254
- await promises_1.default.rm(paths_1.paths.chromium, { recursive: true, force: true });
255
- await (0, mkdirp_1.mkdirp)(paths_1.paths.chromium);
295
+ await promises_2.default.rm(paths_1.paths.chromium, { recursive: true, force: true });
296
+ await promises_2.default.mkdir(paths_1.paths.chromium, { recursive: true });
256
297
  browser = await puppeteer_1.default.launch(launchParams);
257
298
  }
258
299
  else {
@@ -260,7 +301,7 @@ exports.login = {
260
301
  }
261
302
  }
262
303
  // Wait for a bit as sometimes the browser isn't ready.
263
- await bluebird_1.default.delay(200);
304
+ await (0, promises_1.setTimeout)(200);
264
305
  let page;
265
306
  if (incognito) {
266
307
  const existingPages = await browser.pages();
@@ -284,29 +325,27 @@ exports.login = {
284
325
  const samlResponsePromise = new Promise((resolve) => {
285
326
  page.on("request", (req) => {
286
327
  const reqURL = req.url();
287
- debug(`Request: ${reqURL}`);
328
+ const redactedURL = (0, sensitiveOutput_1.redactUrlForLogs)(reqURL);
329
+ debug(`Request: ${redactedURL}`);
288
330
  if (reqURL === AWS_SAML_ENDPOINT ||
289
331
  reqURL === AWS_GOV_SAML_ENDPOINT ||
290
332
  reqURL === AWS_CN_SAML_ENDPOINT) {
291
333
  resolve(undefined);
292
334
  samlResponseData = req.postData();
293
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
294
- req.respond({
335
+ handleBackgroundPromise(req.respond({
295
336
  status: 200,
296
337
  contentType: "text/plain",
297
338
  headers: {},
298
339
  body: "",
299
- });
340
+ }), `Failed to respond to intercepted request ${redactedURL}`);
300
341
  if (browser) {
301
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
302
- browser.close();
342
+ handleBackgroundPromise(browser.close(), "Failed to close browser after receiving SAML response");
303
343
  }
304
344
  browser = undefined;
305
345
  debug(`Received SAML response, browser closed`);
306
346
  }
307
347
  else {
308
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
309
- req.continue();
348
+ handleBackgroundPromise(req.continue(), `Failed to continue intercepted request ${redactedURL}`);
310
349
  }
311
350
  });
312
351
  });
@@ -326,13 +365,12 @@ exports.login = {
326
365
  if (err instanceof Error) {
327
366
  // An error will be thrown if you're still logged in cause the page.goto ot waitForNavigation
328
367
  // will be a redirect to AWS. That's usually OK
329
- debug(`Error occurred during loading the first page: ${err.message}`);
368
+ debug(`Error occurred during loading the first page: ${(0, sensitiveOutput_1.formatDebugErrorMessage)(err)}`);
330
369
  }
331
370
  }
332
371
  if (cliProxy) {
333
372
  let totalUnrecognizedDelay = 0;
334
- // eslint-disable-next-line no-constant-condition
335
- while (true) {
373
+ for (;;) {
336
374
  if (samlResponseData)
337
375
  break;
338
376
  let foundState = false;
@@ -346,7 +384,7 @@ exports.login = {
346
384
  if (err instanceof Error) {
347
385
  // An error can be thrown if the page isn't in a good state.
348
386
  // If one occurs, try again after another loop.
349
- debug(`Error when running state "${state.name}". ${err.toString()}. Retrying...`);
387
+ debug(`Error when running state "${state.name}". ${(0, sensitiveOutput_1.formatDebugErrorMessage)(err)}. Retrying...`);
350
388
  }
351
389
  break;
352
390
  }
@@ -355,7 +393,7 @@ exports.login = {
355
393
  debug(`Found state: ${state.name}`);
356
394
  await Promise.race([
357
395
  samlResponsePromise,
358
- state.handler(page, selected, noPrompt, defaultUsername, defaultPassword, useRememberMe),
396
+ state.handler(page, selected, noPrompt, defaultUsername, defaultPassword, useRememberMe, allowSensitiveStateOutput),
359
397
  ]);
360
398
  debug(`Finished state: ${state.name}`);
361
399
  break;
@@ -367,12 +405,15 @@ exports.login = {
367
405
  else {
368
406
  debug("State not recognized!");
369
407
  if (totalUnrecognizedDelay > MAX_UNRECOGNIZED_PAGE_DELAY) {
408
+ if (!allowSensitiveStateOutput) {
409
+ throw new CLIError_1.CLIError("Unable to recognize page state in a shared environment. Re-run locally with --mode=debug to capture a screenshot.");
410
+ }
370
411
  const path = "az2aws-unrecognized-state.png";
371
412
  await page.screenshot({ path });
372
413
  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`);
373
414
  }
374
415
  totalUnrecognizedDelay += DELAY_ON_UNRECOGNIZED_PAGE;
375
- await bluebird_1.default.delay(DELAY_ON_UNRECOGNIZED_PAGE);
416
+ await (0, promises_1.setTimeout)(DELAY_ON_UNRECOGNIZED_PAGE);
376
417
  }
377
418
  }
378
419
  }
@@ -384,13 +425,15 @@ exports.login = {
384
425
  throw new Error("SAML response not found");
385
426
  }
386
427
  const samlResponse = querystring_1.default.parse(samlResponseData).SAMLResponse;
387
- debug("Found SAML response", samlResponse);
388
428
  if (!samlResponse) {
389
429
  throw new Error("SAML response not found");
390
430
  }
391
431
  else if (Array.isArray(samlResponse)) {
392
432
  throw new Error("SAML can't be an array");
393
433
  }
434
+ debug("Found SAML response", {
435
+ base64Length: samlResponse.length,
436
+ });
394
437
  return samlResponse;
395
438
  }
396
439
  finally {
@@ -408,16 +451,16 @@ exports.login = {
408
451
  _parseRolesFromSamlResponse(assertion) {
409
452
  debug("Converting assertion from base64 to UTF-8");
410
453
  const samlText = Buffer.from(assertion, "base64").toString("utf8");
411
- debug("Converted", samlText);
454
+ debug("Converted assertion from base64 to UTF-8", {
455
+ xmlLength: samlText.length,
456
+ });
412
457
  debug("Parsing SAML XML");
413
458
  const saml = (0, cheerio_1.load)(samlText, { xmlMode: true });
414
459
  debug("Looking for role SAML attribute");
415
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
416
- const roles = saml("Attribute[Name='https://aws.amazon.com/SAML/Attributes/Role']>AttributeValue")
417
- .map(function () {
418
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
419
- // @ts-ignore
420
- const roleAndPrincipal = saml(this).text();
460
+ const roleSelection = saml("Attribute[Name='https://aws.amazon.com/SAML/Attributes/Role']>AttributeValue");
461
+ const roleNodes = roleSelection.toArray();
462
+ const roles = roleNodes.map((roleNode) => {
463
+ const roleAndPrincipal = saml(roleNode).text();
421
464
  const parts = roleAndPrincipal.split(",");
422
465
  // Role / Principal claims may be in either order
423
466
  const [roleIdx, principalIdx] = parts[0].includes(":role/")
@@ -426,9 +469,11 @@ exports.login = {
426
469
  const roleArn = parts[roleIdx].trim();
427
470
  const principalArn = parts[principalIdx].trim();
428
471
  return { roleArn, principalArn };
429
- })
430
- .get();
431
- debug("Found roles", roles);
472
+ });
473
+ debug("Found roles", roles.map((role) => ({
474
+ roleArn: this._redactArnForDebug(role.roleArn),
475
+ principalArn: this._redactArnForDebug(role.principalArn),
476
+ })));
432
477
  return roles;
433
478
  },
434
479
  /**
@@ -437,21 +482,15 @@ exports.login = {
437
482
  * @param {bool} [noPrompt] - Enable skipping of user prompting
438
483
  * @param {string} [defaultRoleArn] - The default role ARN
439
484
  * @param {number} [defaultDurationHours] - The default session duration in hours
485
+ * @param {string} [nonInteractiveModeLabel] - CLI flag label to reference in
486
+ * non-interactive errors
440
487
  * @returns {Promise.<{role: string, durationHours: number}>} The selected role and duration
441
488
  * @private
442
489
  */
443
- async _askUserForRoleAndDurationAsync(roles, noPrompt, defaultRoleArn, defaultDurationHours) {
490
+ async _askUserForRoleAndDurationAsync(roles, noPrompt, defaultRoleArn, defaultDurationHours, nonInteractiveModeLabel = "--no-prompt") {
491
+ var _a;
444
492
  let role;
445
- let durationHours = 1;
446
- if (defaultDurationHours) {
447
- const parsedDuration = parseInt(defaultDurationHours, 10);
448
- if (!Number.isNaN(parsedDuration) &&
449
- parsedDuration > 0 &&
450
- parsedDuration <= 12) {
451
- durationHours = parsedDuration;
452
- }
453
- }
454
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
493
+ let durationHours = (_a = (0, sessionDuration_1.parseSessionDurationHours)(defaultDurationHours)) !== null && _a !== void 0 ? _a : 1;
455
494
  const questions = [];
456
495
  if (roles.length === 0) {
457
496
  throw new CLIError_1.CLIError("No roles found in SAML response.");
@@ -463,11 +502,11 @@ exports.login = {
463
502
  else {
464
503
  if (noPrompt) {
465
504
  if (!defaultRoleArn) {
466
- throw new CLIError_1.CLIError("--no-prompt requires azure_default_role_arn when multiple roles are available.");
505
+ throw new CLIError_1.CLIError(`${nonInteractiveModeLabel} requires azure_default_role_arn when multiple roles are available.`);
467
506
  }
468
507
  role = roles.find((r) => r.roleArn === defaultRoleArn);
469
508
  if (!role) {
470
- throw new CLIError_1.CLIError(`Default role ARN '${defaultRoleArn}' was not found in the SAML response.`);
509
+ throw new CLIError_1.CLIError("Configured default role ARN was not found in the SAML response.");
471
510
  }
472
511
  debug("Valid role found. No need to ask.");
473
512
  }
@@ -495,23 +534,23 @@ exports.login = {
495
534
  name: "durationHours",
496
535
  message: "Session Duration Hours (up to 12):",
497
536
  type: "input",
498
- default: defaultDurationHours || 1,
499
- validate: (input) => {
500
- const num = Number(input);
501
- if (num > 0 && num <= 12)
502
- return true;
503
- return "Duration hours must be between 1 and 12";
504
- },
537
+ default: String(durationHours),
538
+ validate: sessionDuration_1.validateSessionDurationHours,
505
539
  });
506
540
  }
507
541
  // Don't prompt for questions if not needed, an unneeded TTYWRAP prevents node from exiting when
508
542
  // user is logged in and using multiple profiles --all-profiles and --no-prompt
509
543
  if (questions.length > 0) {
510
544
  const answers = await inquirer_1.default.prompt(questions);
511
- if (!role)
545
+ if (!role && answers.role) {
512
546
  role = roles.find((r) => r.roleArn === answers.role);
547
+ }
513
548
  if (answers.durationHours) {
514
- durationHours = parseInt(answers.durationHours, 10);
549
+ const parsedDurationHours = (0, sessionDuration_1.parseSessionDurationHours)(answers.durationHours);
550
+ if (parsedDurationHours === null) {
551
+ throw new CLIError_1.CLIError(sessionDuration_1.sessionDurationHoursValidationMessage);
552
+ }
553
+ durationHours = parsedDurationHours;
515
554
  }
516
555
  }
517
556
  if (!role) {
@@ -523,16 +562,19 @@ exports.login = {
523
562
  * Assume the role.
524
563
  * @param {string} profileName - The profile name
525
564
  * @param {string} assertion - The SAML assertion
526
- * @param {string} role - The role to assume
565
+ * @param {Role} role - The role to assume
527
566
  * @param {number} durationHours - The session duration in hours
528
- * @param {bool} awsNoVerifySsl - Whether to have the AWS CLI verify SSL
567
+ * @param {boolean} awsNoVerifySsl - Whether the AWS SDK STS client should
568
+ * disable TLS certificate verification
529
569
  * @param {string} region - AWS region, if specified
530
- * @returns {Promise} A promise
570
+ * @param {boolean} writeProfile - Whether to persist the credentials to the
571
+ * AWS shared credentials file
572
+ * @returns {Promise<AwsCredentials | undefined>} Retrieved credentials, or
573
+ * undefined when STS does not return them
531
574
  * @private
532
575
  */
533
- async _assumeRoleAsync(profileName, assertion, role, durationHours, awsNoVerifySsl, region) {
534
- var _a, _b, _c, _d, _e;
535
- console.log(`Assuming role ${role.roleArn} in region ${region}...`);
576
+ async _assumeRoleAsync(profileName, assertion, role, durationHours, awsNoVerifySsl, region, writeProfile = true) {
577
+ console.log(`Assuming selected role in region ${region}...`);
536
578
  let stsOptions = {};
537
579
  if (awsNoVerifySsl) {
538
580
  console.warn("WARNING: SSL certificate verification is disabled. " +
@@ -574,13 +616,25 @@ exports.login = {
574
616
  });
575
617
  if (!res.Credentials) {
576
618
  debug("Unable to get security credentials from AWS");
577
- return;
619
+ return undefined;
578
620
  }
579
- await awsConfig_1.awsConfig.setProfileCredentialsAsync(profileName, {
580
- aws_access_key_id: (_a = res.Credentials.AccessKeyId) !== null && _a !== void 0 ? _a : "",
581
- aws_secret_access_key: (_b = res.Credentials.SecretAccessKey) !== null && _b !== void 0 ? _b : "",
582
- aws_session_token: (_c = res.Credentials.SessionToken) !== null && _c !== void 0 ? _c : "",
583
- aws_expiration: (_e = (_d = res.Credentials.Expiration) === null || _d === void 0 ? void 0 : _d.toISOString()) !== null && _e !== void 0 ? _e : "",
584
- });
621
+ if (!res.Credentials.AccessKeyId ||
622
+ !res.Credentials.SecretAccessKey ||
623
+ !res.Credentials.SessionToken ||
624
+ !res.Credentials.Expiration) {
625
+ debug("Received incomplete credentials from AWS");
626
+ throw new CLIError_1.CLIError("AWS returned incomplete credentials. One or more required fields " +
627
+ "(AccessKeyId, SecretAccessKey, SessionToken, Expiration) are missing.");
628
+ }
629
+ const credentials = {
630
+ aws_access_key_id: res.Credentials.AccessKeyId,
631
+ aws_secret_access_key: res.Credentials.SecretAccessKey,
632
+ aws_session_token: res.Credentials.SessionToken,
633
+ aws_expiration: res.Credentials.Expiration.toISOString(),
634
+ };
635
+ if (writeProfile) {
636
+ await awsConfig_1.awsConfig.setProfileCredentialsAsync(profileName, credentials);
637
+ }
638
+ return credentials;
585
639
  },
586
640
  };