From 26128478219a8b7fc8e1c18492058dd1ccad7f13 Mon Sep 17 00:00:00 2001 From: Hopper Date: Thu, 14 Sep 2023 17:05:34 -0400 Subject: [PATCH 1/5] Updated to work with the latest AWS SDK (v3) and fixed a race condition with Okta. --- Dockerfile | 2 +- package.json | 8 ++++---- src/providers/okta-mfa.js | 3 +++ src/providers/okta.js | 2 ++ src/token-getter.js | 22 ++++++++++++---------- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3495aea..018b435 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:8-slim +FROM node:18-slim WORKDIR /usr/src/app diff --git a/package.json b/package.json index 1a8d7c6..699be1b 100644 --- a/package.json +++ b/package.json @@ -32,22 +32,22 @@ }, "homepage": "https://github.com/meetearnest/aws-sts#readme", "dependencies": { + "@aws-sdk/client-sts": "^3.410.0", "argparse": "^1.0.9", - "aws-sdk": "^2.478.0", "clui": "^0.3.1", "co": "^4.6.0", "coinquirer": "0.0.5", "colors": "^1.1.2", "ini": "^1.3.4", "mkdirp": "^0.5.1", - "nightmare": "^2.10.0", + "nightmare": "^2.8.0", "thunkify": "^2.1.2", - "xml2js": "^0.4.17" + "xml2js": "^0.6.2" }, "devDependencies": { "@earnest/eslint-config": "latest", "eslint": "~5.3.0", "eslint-plugin-mocha": "~5.3.0", - "mocha": "~6.1.4" + "mocha": "^10.2.0" } } diff --git a/src/providers/okta-mfa.js b/src/providers/okta-mfa.js index b3549ae..06aeb98 100644 --- a/src/providers/okta-mfa.js +++ b/src/providers/okta-mfa.js @@ -4,6 +4,7 @@ const clui = require('clui'); const OktaHelpers = require('./okta-helpers'); const GoogleAuthenticator = { + name: "GoogleAuthenticator", detect: function *(nightmare) { return yield nightmare.visible('.mfa-verify-totp'); }, @@ -41,6 +42,8 @@ const GoogleAuthenticator = { }; const OktaVerify = { + name: "OktaVerify", + detect: function *(nightmare) { return yield nightmare.visible('.mfa-verify-push'); }, diff --git a/src/providers/okta.js b/src/providers/okta.js index 7734bdb..25815fc 100644 --- a/src/providers/okta.js +++ b/src/providers/okta.js @@ -59,6 +59,8 @@ const Okta = { .type('input[name="username"]', username) .click('input[type="submit"]') // Submit form .wait('.o-form-input-name-password') + .wait('input[type="submit"]') // Form is loaded via AJAX + .wait(300) .type('input[name="password"]', password) .click('input[type="submit"]') // Submit form .wait('.o-form-has-errors, .mfa-verify') // Wait for error or success diff --git a/src/token-getter.js b/src/token-getter.js index 880a027..66be3c0 100644 --- a/src/token-getter.js +++ b/src/token-getter.js @@ -1,12 +1,12 @@ 'use strict'; -const AWS = require('aws-sdk'); +const { STSClient, AssumeRoleWithSAMLCommand, AssumeRoleCommand } = require("@aws-sdk/client-sts"); const clui = require('clui'); // Not thread safe! class TokenGetter { constructor(config) { this.spinner = new clui.Spinner('Getting token...'); - this.sts = new AWS.STS({region: config.region}); + this.sts = new STSClient({region: config.region}); this.defaultAccount = config.defaultAccount; } @@ -46,26 +46,28 @@ class TokenGetter { } async getSTSToken() { - const request = this.sts.assumeRoleWithSAML({ + const command = new AssumeRoleWithSAMLCommand({ PrincipalArn: this.role.principalArn, RoleArn: this.role.roleArn, SAMLAssertion: this.samlAssertion }); - return await request.promise(); + return await this.sts.send(command); } async getAssumeRoleToken(originalToken) { - this.sts.config.credentials = new AWS.Credentials( - originalToken.Credentials.AccessKeyId, - originalToken.Credentials.SecretAccessKey, - originalToken.Credentials.SessionToken); + this.sts.config.credentials = { + accessKeyId: originalToken.Credentials.AccessKeyId, + secretAccessKey: originalToken.Credentials.SecretAccessKey, + sessionToken: originalToken.Credentials.SessionToken + }; const roleArn = this.role.roleArn.replace(/::(\d+)/, `::${this.accountNumber}`); const splitArn = originalToken.AssumedRoleUser.Arn.split('/'); - return await this.sts.assumeRole({ + const command = new AssumeRoleCommand({ RoleArn: roleArn, RoleSessionName: splitArn[splitArn.length - 1] - }).promise(); + }); + return await this.sts.send(command); } } From b05b105063741bb529c7b8ba54668cae46eff811 Mon Sep 17 00:00:00 2001 From: Andy Hopper Date: Mon, 18 Sep 2023 17:09:07 -0400 Subject: [PATCH 2/5] Fixes for push verification (added waits). --- src/providers/okta-mfa.js | 4 +++- src/providers/okta.js | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/providers/okta-mfa.js b/src/providers/okta-mfa.js index 06aeb98..4b464b5 100644 --- a/src/providers/okta-mfa.js +++ b/src/providers/okta-mfa.js @@ -45,7 +45,9 @@ const OktaVerify = { name: "OktaVerify", detect: function *(nightmare) { - return yield nightmare.visible('.mfa-verify-push'); + return yield nightmare + .wait(500) + .visible('.mfa-verify-push'); }, prompt: function *() { diff --git a/src/providers/okta.js b/src/providers/okta.js index 25815fc..aa55b54 100644 --- a/src/providers/okta.js +++ b/src/providers/okta.js @@ -32,7 +32,7 @@ const Okta = { openDevTools: true, typeInterval: 5, pollInterval: 10, - waitTimeout: 30 * 1000 + waitTimeout: 300 * 1000 }); let hasError = yield nightmare .on('console', function (type, message) { @@ -55,14 +55,15 @@ const Okta = { .goto(idpEntryUrl) .visible('.primary-auth-form') .wait('input[type="submit"]') // Form is loaded via AJAX - .wait(300) + .wait(500) .type('input[name="username"]', username) .click('input[type="submit"]') // Submit form .wait('.o-form-input-name-password') .wait('input[type="submit"]') // Form is loaded via AJAX - .wait(300) + .wait(500) .type('input[name="password"]', password) .click('input[type="submit"]') // Submit form + .wait(1000) // wait for the MFA form (or an error) to render .wait('.o-form-has-errors, .mfa-verify') // Wait for error or success .exists('.o-form-has-errors'); spinner.stop(); From c8ec5d844e496a91e4033dbdec1f929f2633d707 Mon Sep 17 00:00:00 2001 From: Andy Hopper Date: Mon, 18 Sep 2023 17:10:09 -0400 Subject: [PATCH 3/5] Fix for [DEP0005] ('new Buffer()') deprecation warning. --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index bd78012..4a0453f 100644 --- a/src/index.js +++ b/src/index.js @@ -87,7 +87,7 @@ function parseArgs(providerName) { } function *selectRole(samlAssertion, roleName) { - let buf = new Buffer(samlAssertion, 'base64'); + let buf = Buffer.alloc(samlAssertion.length, samlAssertion, 'base64'); let saml = yield thunkify(xml2js.parseString)( buf, {tagNameProcessors: [xml2js.processors.stripPrefix], xmlns: true}); From 75375667e1b96bd1e5cc586724964194809dfc85 Mon Sep 17 00:00:00 2001 From: Andy Hopper Date: Tue, 19 Sep 2023 10:39:02 -0400 Subject: [PATCH 4/5] Added 'duration' argument, modified logic to accomodate multiple accounts with the same role name. --- src/index.js | 56 +++++++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/index.js b/src/index.js index bd78012..230b03a 100644 --- a/src/index.js +++ b/src/index.js @@ -25,12 +25,13 @@ co(function *() { const args = parseArgs(provider.name); const tokenGetter = new TokenGetter(config); const account = config.accounts[args.account]; - const idpEntryUrl = account.idpEntryUrl ? account.idpEntryUrl : config.idpEntryUrl; + const durationSeconds = args.durationSeconds; + const idpEntryUrl = (account && account.idpEntryUrl) ? account.idpEntryUrl : config.idpEntryUrl; account.name = args.account; const samlAssertion = yield provider.login(idpEntryUrl, args.username, args.password, args.otp); - const role = yield selectRole(samlAssertion, args.role); - const token = yield tokenGetter.getToken(samlAssertion, account, role); + const role = yield selectRole(samlAssertion, args.role, account); + const token = yield tokenGetter.getToken(samlAssertion, account, role, durationSeconds); const profileName = buildProfileName(role, account.name, args.profile); yield writeTokenToConfig(token, profileName); @@ -83,10 +84,13 @@ function parseArgs(providerName) { help: 'Profile name that the AWS credentials should be saved as. ' + 'Defaults to the name of the account specified.' }); + parser.addArgument(['--durationSeconds'], { + help: 'Duration of the session, in seconds.' + }); return parser.parseArgs(); } -function *selectRole(samlAssertion, roleName) { +function *selectRole(samlAssertion, roleName, account) { let buf = new Buffer(samlAssertion, 'base64'); let saml = yield thunkify(xml2js.parseString)( buf, @@ -115,29 +119,31 @@ function *selectRole(samlAssertion, roleName) { }); let multipleAccounts = accountIds.length > 1; - // Set the default role if one was passed - let role = roles.find(r => r.name === roleName); + // Multiple accounts may have this role name! + // Make sure that there is only one role/account + // pair matching this name + let role = roles.find(r => r.name === roleName && r.accountId === account.accountNumber); + if (!role) { role = roles[0]; // Couldn't find that role, default to the first one - } - - if (roles.length > 1 && !roleName) { - let ci = new coinquirer(); - role = yield ci.prompt({ - type: 'list', - message: 'Please select a role:', - choices: roles.map(r => { - let name = r.name; - if (multipleAccounts) { - name += ' (' + r.accountId + ')'; - } - - return { - name: name, - value: r - }; - }) - }); + if (roles.length > 1) { + let ci = new coinquirer(); + role = yield ci.prompt({ + type: 'list', + message: 'Please select a role:', + choices: roles.map(r => { + let name = r.name; + if (multipleAccounts) { + name += ' (' + r.accountId + ')'; + } + + return { + name: name, + value: r + }; + }) + }); + } } return role; From 917a7f08c4e26812361fca3bb094b8560f03d978 Mon Sep 17 00:00:00 2001 From: Andy Hopper Date: Tue, 19 Sep 2023 10:40:09 -0400 Subject: [PATCH 5/5] Added logic to add DurationSeconds when suppplied at the command line. --- src/token-getter.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/token-getter.js b/src/token-getter.js index 66be3c0..9de8471 100644 --- a/src/token-getter.js +++ b/src/token-getter.js @@ -10,11 +10,12 @@ class TokenGetter { this.defaultAccount = config.defaultAccount; } - async getToken(samlAssertion, account, role) { + async getToken(samlAssertion, account, role, durationSeconds) { this.samlAssertion = samlAssertion; this.account = account; this.accountNumber = this.account.accountNumber; this.role = role; + this.durationSeconds = durationSeconds; try { this.spinner.start(); @@ -51,6 +52,10 @@ class TokenGetter { RoleArn: this.role.roleArn, SAMLAssertion: this.samlAssertion }); + if (this.durationSeconds) + { + command.input.DurationSeconds = this.durationSeconds; + } return await this.sts.send(command); }