UNPKG

@fiftyten/db-toolkit

Version:

Complete database toolkit: connections, migration, and operations via AWS Session Manager

337 lines 13.5 kB
"use strict"; 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