@fiftyten/db-connect
Version:
CLI tool for database connections and DynamoDB operations via AWS Session Manager
288 lines • 11.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MfaAuthenticator = void 0;
const client_sts_1 = require("@aws-sdk/client-sts");
const client_iam_1 = require("@aws-sdk/client-iam");
const inquirer_1 = __importDefault(require("inquirer"));
const chalk_1 = __importDefault(require("chalk"));
class MfaAuthenticator {
constructor(region = 'us-west-1') {
this.region = region;
this.stsClient = new client_sts_1.STSClient({ region });
this.iamClient = new client_iam_1.IAMClient({ region });
}
/**
* Check if current credentials are MFA-authenticated
*/
async checkMfaStatus() {
try {
const command = new client_sts_1.GetCallerIdentityCommand({});
const response = await this.stsClient.send(command);
// If we can call GetCallerIdentity and the ARN contains 'assumed-role',
// we're likely using temporary credentials (MFA session)
return response.Arn?.includes('assumed-role') || false;
}
catch (error) {
return false;
}
}
/**
* Detect if an error is due to MFA requirement
*/
isMfaRequired(error) {
const errorMessage = error?.message || '';
const errorCode = error?.Code || error?.name || '';
return (errorMessage.includes('explicit deny') ||
errorMessage.includes('MFA') ||
errorMessage.includes('MultiFactorAuthentication') ||
errorCode === 'AccessDenied' ||
errorCode === 'AccessDeniedException');
}
/**
* Auto-discover MFA devices for current user
*/
async discoverMfaDevices() {
try {
// First, get current user identity
const identityCommand = new client_sts_1.GetCallerIdentityCommand({});
const identityResponse = await this.stsClient.send(identityCommand);
if (!identityResponse.Arn) {
return [];
}
// Extract username from ARN
const arnParts = identityResponse.Arn.split('/');
const username = arnParts[arnParts.length - 1];
// List MFA devices for the user
const mfaCommand = new client_iam_1.ListMFADevicesCommand({
UserName: username
});
const mfaResponse = await this.iamClient.send(mfaCommand);
return mfaResponse.MFADevices?.map(device => device.SerialNumber) || [];
}
catch (error) {
// If we can't list MFA devices, try fallback detection
console.log(chalk_1.default.yellow('Could not auto-discover MFA devices, using fallback detection'));
return [];
}
}
/**
* Automatically detect MFA configuration from AWS config
*/
async detectMfaConfig() {
try {
// Try to auto-discover MFA devices
const mfaDevices = await this.discoverMfaDevices();
if (mfaDevices.length > 0) {
return {
mfaSerial: mfaDevices[0], // Use first available device
region: this.region
};
}
// Fallback: Try to get current identity to determine MFA device
const command = new client_sts_1.GetCallerIdentityCommand({});
const response = await this.stsClient.send(command);
if (response.Arn && response.UserId) {
// Extract account and username from ARN
const accountId = response.Account;
const arnParts = response.Arn.split('/');
const username = arnParts[arnParts.length - 1];
// Common MFA device patterns
const mfaSerial = `arn:aws:iam::${accountId}:mfa/${username}`;
return {
mfaSerial,
region: this.region
};
}
}
catch (error) {
// Ignore errors during detection
}
return null;
}
/**
* Prompt user for MFA configuration
*/
async promptMfaConfig(detectedConfig) {
console.log(chalk_1.default.yellow('🔐 MFA authentication required'));
// Try to discover available MFA devices
const availableDevices = await this.discoverMfaDevices();
if (availableDevices.length === 0) {
console.log(chalk_1.default.gray('Please provide your MFA device serial number:'));
console.log('');
const questions = [
{
type: 'input',
name: 'mfaSerial',
message: 'MFA Device Serial Number:',
default: detectedConfig?.mfaSerial,
validate: (input) => {
if (!input || !input.startsWith('arn:aws:iam::')) {
return 'Please enter a valid MFA device ARN (arn:aws:iam::ACCOUNT:mfa/DEVICE_NAME)';
}
return true;
}
}
];
const answers = await inquirer_1.default.prompt(questions);
return {
mfaSerial: answers.mfaSerial,
region: this.region
};
}
else if (availableDevices.length === 1) {
// Auto-select single device
console.log(chalk_1.default.green(`✅ Auto-detected MFA device: ${availableDevices[0]}`));
return {
mfaSerial: availableDevices[0],
region: this.region
};
}
else {
// Multiple devices - let user choose
console.log(chalk_1.default.gray('Multiple MFA devices found. Please select one:'));
console.log('');
const questions = [
{
type: 'list',
name: 'mfaSerial',
message: 'Select MFA Device:',
choices: availableDevices.map(device => ({
name: device.split('/').pop() + ` (${device})`,
value: device
}))
}
];
const answers = await inquirer_1.default.prompt(questions);
return {
mfaSerial: answers.mfaSerial,
region: this.region
};
}
}
/**
* Prompt for MFA token
*/
async promptMfaToken() {
const answer = await inquirer_1.default.prompt([
{
type: 'input',
name: 'token',
message: 'Enter MFA token code:',
validate: (input) => {
if (!input || input.length !== 6 || !/^\d{6}$/.test(input)) {
return 'Please enter a 6-digit MFA token code';
}
return true;
}
}
]);
return answer.token;
}
/**
* Get session token with MFA (for users with MFA enforcement policies)
*/
async getSessionTokenWithMfa(config, tokenCode) {
const command = new client_sts_1.GetSessionTokenCommand({
SerialNumber: config.mfaSerial,
TokenCode: tokenCode,
DurationSeconds: 3600 // 1 hour
});
try {
const response = await this.stsClient.send(command);
if (!response.Credentials) {
throw new Error('No credentials returned from STS');
}
return {
accessKeyId: response.Credentials.AccessKeyId,
secretAccessKey: response.Credentials.SecretAccessKey,
sessionToken: response.Credentials.SessionToken,
expiration: response.Credentials.Expiration
};
}
catch (error) {
if (error instanceof Error) {
if (error.message.includes('MultiFactorAuthentication')) {
throw new Error('Invalid MFA token code. Please try again.');
}
if (error.message.includes('TokenCode')) {
throw new Error('Invalid or expired MFA token code.');
}
if (error.message.includes('AccessDenied')) {
throw new Error('Access denied. Please check your MFA device serial number.');
}
}
throw error;
}
}
/**
* Assume role with MFA
*/
async assumeRoleWithMfa(config, tokenCode) {
const command = new client_sts_1.AssumeRoleCommand({
RoleArn: config.roleArn,
RoleSessionName: config.sessionName || 'fiftyten-db-session',
SerialNumber: config.mfaSerial,
TokenCode: tokenCode,
DurationSeconds: 3600 // 1 hour
});
try {
const response = await this.stsClient.send(command);
if (!response.Credentials) {
throw new Error('No credentials returned from STS');
}
return {
accessKeyId: response.Credentials.AccessKeyId,
secretAccessKey: response.Credentials.SecretAccessKey,
sessionToken: response.Credentials.SessionToken,
expiration: response.Credentials.Expiration
};
}
catch (error) {
if (error instanceof Error) {
if (error.message.includes('MultiFactorAuthentication')) {
throw new Error('Invalid MFA token code. Please try again.');
}
if (error.message.includes('TokenCode')) {
throw new Error('Invalid or expired MFA token code.');
}
if (error.message.includes('AccessDenied')) {
throw new Error('Access denied. Please check your MFA role ARN and device serial number.');
}
}
throw error;
}
}
/**
* Full MFA authentication flow
*/
async authenticateWithMfa() {
console.log(chalk_1.default.blue('🔒 Starting MFA authentication...'));
// Try to detect MFA configuration
const detectedConfig = await this.detectMfaConfig();
// Prompt for configuration
const config = await this.promptMfaConfig(detectedConfig);
// Prompt for MFA token
const tokenCode = await this.promptMfaToken();
// Get session token with MFA (not role assumption)
console.log(chalk_1.default.gray('Getting MFA session token...'));
const credentials = await this.getSessionTokenWithMfa(config, tokenCode);
console.log(chalk_1.default.green('✅ MFA authentication successful!'));
console.log(chalk_1.default.gray(`Session expires: ${credentials.expiration?.toLocaleString()}`));
console.log('');
return credentials;
}
/**
* Apply MFA credentials to AWS SDK clients
*/
applyCredentials(credentials) {
// Set environment variables for AWS SDK
process.env.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
process.env.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey;
process.env.AWS_SESSION_TOKEN = credentials.sessionToken;
}
}
exports.MfaAuthenticator = MfaAuthenticator;
//# sourceMappingURL=mfa-auth.js.map