Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:8-slim
FROM node:18-slim

WORKDIR /usr/src/app

Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
58 changes: 32 additions & 26 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -83,11 +84,14 @@ 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) {
let buf = new Buffer(samlAssertion, 'base64');
function *selectRole(samlAssertion, roleName, account) {
let buf = Buffer.alloc(samlAssertion.length, samlAssertion, 'base64');
let saml = yield thunkify(xml2js.parseString)(
buf,
{tagNameProcessors: [xml2js.processors.stripPrefix], xmlns: true});
Expand Down Expand Up @@ -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;
Expand Down
7 changes: 6 additions & 1 deletion src/providers/okta-mfa.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
},
Expand Down Expand Up @@ -41,8 +42,12 @@ const GoogleAuthenticator = {
};

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 *() {
Expand Down
7 changes: 5 additions & 2 deletions src/providers/okta.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -55,12 +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(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();
Expand Down
29 changes: 18 additions & 11 deletions src/token-getter.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
'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;
}

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();
Expand Down Expand Up @@ -46,26 +47,32 @@ 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();
if (this.durationSeconds)
{
command.input.DurationSeconds = this.durationSeconds;
}
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);
}
}

Expand Down