UNPKG

@fiftyten/db-connect

Version:

CLI tool for database connections and DynamoDB operations via AWS Session Manager

288 lines 11.6 kB
"use strict"; 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