@fiftyten/db-toolkit
Version:
Complete database toolkit: connections, migration, and operations via AWS Session Manager
337 lines • 13.5 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
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 readline = __importStar(require("readline"));
const chalk_1 = __importDefault(require("chalk"));
// Helper functions for readline prompts
function promptInput(message, defaultValue) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const prompt = defaultValue ? `${message} (${defaultValue}): ` : `${message}: `;
return new Promise((resolve) => {
rl.question(prompt, (answer) => {
rl.close();
resolve(answer.trim() || defaultValue || '');
});
});
}
function promptChoice(message, choices) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log(message);
choices.forEach((choice, index) => {
console.log(`${index + 1}. ${choice.name}`);
});
return new Promise((resolve) => {
rl.question('Select option (number): ', (answer) => {
rl.close();
const index = parseInt(answer) - 1;
if (index >= 0 && index < choices.length) {
resolve(choices[index].value);
}
else {
console.log('Invalid selection, using first option');
resolve(choices[0].value);
}
});
});
}
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('');
let mfaSerial = '';
while (!mfaSerial || !mfaSerial.startsWith('arn:aws:iam::')) {
mfaSerial = await promptInput('MFA Device Serial Number', detectedConfig?.mfaSerial);
if (!mfaSerial || !mfaSerial.startsWith('arn:aws:iam::')) {
console.log(chalk_1.default.red('Please enter a valid MFA device ARN (arn:aws:iam::ACCOUNT:mfa/DEVICE_NAME)'));
}
}
return {
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 choices = availableDevices.map(device => ({
name: device.split('/').pop() + ` (${device})`,
value: device
}));
const mfaSerial = await promptChoice('Select MFA Device:', choices);
return {
mfaSerial,
region: this.region
};
}
}
/**
* Prompt for MFA token
*/
async promptMfaToken() {
let token = '';
while (!token || token.length !== 6 || !/^\d{6}$/.test(token)) {
token = await promptInput('Enter MFA token code');
if (!token || token.length !== 6 || !/^\d{6}$/.test(token)) {
console.log(chalk_1.default.red('Please enter a 6-digit MFA token code'));
}
}
return 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