UNPKG

@aashari/mcp-server-aws-sso

Version:

Node.js/TypeScript MCP server for AWS Single Sign-On (SSO). Enables AI systems (LLMs) with tools to initiate SSO login (device auth flow), list accounts/roles, and securely execute AWS CLI commands using temporary credentials. Streamlines AI interaction w

319 lines (317 loc) 15.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const logger_util_js_1 = require("../utils/logger.util.js"); const error_handler_util_js_1 = require("../utils/error-handler.util.js"); const error_types_util_js_1 = require("../utils/error-types.util.js"); const vendor_aws_sso_auth_core_service_js_1 = require("../services/vendor.aws.sso.auth.core.service.js"); const vendor_aws_sso_auth_service_js_1 = require("../services/vendor.aws.sso.auth.service.js"); const vendor_aws_sso_accounts_service_js_1 = require("../services/vendor.aws.sso.accounts.service.js"); const aws_sso_auth_formatter_js_1 = require("./aws.sso.auth.formatter.js"); const formatter_util_js_1 = require("../utils/formatter.util.js"); /** * AWS SSO Authentication Controller Module * * Provides functionality for authenticating with AWS SSO, initiating the login flow, * and retrieving temporary credentials. Handles browser-based authentication, * token management, and credential retrieval. */ // Create a module logger const controllerLogger = logger_util_js_1.Logger.forContext('controllers/aws.sso.auth.controller.ts'); // Log module initialization controllerLogger.debug('AWS SSO authentication controller initialized'); /** * Start the AWS SSO login process * * Initiates the device authorization flow, displays verification instructions, * and starts background polling for authentication completion. * * @async * @param {Object} [params] - Optional parameters for login * @param {boolean} [params.launchBrowser=true] - Whether to automatically launch a browser with the verification URI * @returns {Promise<ControllerResponse>} Response with login instructions and background polling started * @throws {Error} If login initialization fails * @example * // Start login with automatic browser launch and background polling * const result = await startLogin(); * * // Start login without browser launch but with background polling * const result = await startLogin({ launchBrowser: false }); */ async function startLogin(params) { const loginLogger = logger_util_js_1.Logger.forContext('controllers/aws.sso.auth.controller.ts', 'startLogin'); loginLogger.debug('Starting AWS SSO login process'); loginLogger.info('Initiating device authorization flow for AWS SSO'); // Get browser launch preference const launchBrowser = params?.launchBrowser ?? true; try { // Forcibly clear any existing authorization data to ensure a fresh start loginLogger.debug('Clearing any existing device authorization data'); try { // Import the cache utility to directly clear the auth data const ssoCache = await import('../utils/aws.sso.cache.util.js'); await ssoCache.clearDeviceAuthorizationInfo(); loginLogger.debug('Successfully cleared device authorization cache'); } catch (clearError) { loginLogger.warn('Error clearing device authorization data', clearError); // Continue even if clearing fails } // Check if we already have a valid token const cachedToken = await (0, vendor_aws_sso_auth_core_service_js_1.getCachedSsoToken)(); if (cachedToken) { loginLogger.debug('Found valid token in cache'); // Format expiration date for display let expiresDate = 'Unknown'; try { if (cachedToken.expiresAt) { const expirationDate = new Date(cachedToken.expiresAt * 1000); expiresDate = expirationDate.toLocaleString(); } } catch (error) { loginLogger.error('Error formatting expiration date', error); } // Don't try to list accounts, which might fail - just show that we're already logged in return { content: (0, aws_sso_auth_formatter_js_1.formatAlreadyLoggedIn)(expiresDate), }; } // Start the login flow loginLogger.debug('No valid token found, initiating new login flow'); const deviceAuth = await (0, vendor_aws_sso_auth_service_js_1.startSsoLogin)(); // Launch browser if enabled let browserLaunched = false; if (launchBrowser) { try { // AWS SSO provides a complete URI that includes the user code // This is the preferred URL to launch in the browser const verificationUrl = deviceAuth.verificationUriComplete; if (!verificationUrl) { loginLogger.debug('No verificationUriComplete provided, browser launch might not work properly'); } loginLogger.debug('Attempting to launch browser', { verificationUri: verificationUrl || deviceAuth.verificationUri, userCode: deviceAuth.userCode, }); // Use dynamic import for 'open' package const openModule = await import('open'); const open = openModule.default; // Try to open the browser with the verification URI // Important: Use the complete URI that includes the user code if available await open(verificationUrl || deviceAuth.verificationUri); browserLaunched = true; loginLogger.debug('Browser launched successfully with URL:', verificationUrl || deviceAuth.verificationUri); } catch (browserError) { loginLogger.error('Failed to launch browser', browserError); // Browser launch failed, but continue with manual instructions browserLaunched = false; } } else { loginLogger.debug('Browser launch disabled'); } // Build initial response based on whether browser was launched let initialContent; if (browserLaunched) { // Even when browser is launched, still include manual instructions // so users have the information if they need it initialContent = (0, aws_sso_auth_formatter_js_1.formatLoginWithBrowserLaunch)(deviceAuth.verificationUri, deviceAuth.userCode) + '\n\n' + (0, aws_sso_auth_formatter_js_1.formatLoginManual)(deviceAuth.verificationUri, deviceAuth.userCode); } else { initialContent = (0, aws_sso_auth_formatter_js_1.formatLoginManual)(deviceAuth.verificationUri, deviceAuth.userCode); } // Display the login instructions loginLogger.info(initialContent); // Start background polling (non-blocking) loginLogger.debug('Starting background polling for authentication'); loginLogger.info('Background polling started - authentication will be processed automatically'); // Start polling in the background without blocking the response // Use setImmediate to ensure this runs after the response is sent setImmediate(async () => { try { loginLogger.debug('Background polling: Starting token polling'); const authResult = await (0, vendor_aws_sso_auth_service_js_1.pollForSsoToken)(); loginLogger.info('Background polling: Authentication successful, token received', { expiresAt: authResult.expiresAt, }); } catch (error) { loginLogger.error('Background polling: Authentication failed', error); // In background mode, we just log the error and don't throw // The user can check status using aws_sso_status tool } }); // Add device info to the content for clarity const deviceInfoContent = ` ## Authentication Details - Verification Code: **${deviceAuth.userCode}** - Browser ${browserLaunched ? 'Launched' : 'Not Launched'}: ${browserLaunched ? 'Yes' : 'No'} - Verification URL: ${deviceAuth.verificationUri} - Code Expires In: ${Math.floor(deviceAuth.expiresIn / 60)} minutes - Background Polling: **Active** (credentials will be collected automatically) Complete the authentication in your browser. Use 'aws_sso_status' to check completion status, or proceed with other AWS commands once authenticated.`; // Return instructions immediately with background polling active return { content: initialContent + deviceInfoContent, }; } catch (error) { // Handle startup errors - throw directly without retry const errorContext = (0, error_types_util_js_1.buildErrorContext)('AWS SSO Authentication', 'login', 'controllers/aws.sso.auth.controller.ts@startLogin', undefined, { launchBrowser, }); throw (0, error_handler_util_js_1.handleControllerError)(error, errorContext); } } /** * Get AWS credentials for a specific role * * Retrieves temporary AWS credentials for a specific account and role * that can be used for AWS API calls. Uses cached credentials if available. * * @async * @param {Object} params - Credential parameters * @param {string} params.accessToken - AWS SSO access token * @param {string} params.accountId - AWS account ID * @param {string} params.roleName - IAM role name to get credentials for * @returns {Promise<ControllerResponse>} Response with credential status and formatted output * @throws {Error} If credential retrieval fails or authentication is invalid * @example * // Get credentials for role AdminAccess in account 123456789012 * const result = await getCredentials({ * accessToken: "token-value", * accountId: "123456789012", * roleName: "AdminAccess" * }); */ async function getCredentials(params) { const credentialsLogger = logger_util_js_1.Logger.forContext('controllers/aws.sso.auth.controller.ts', 'getCredentials'); credentialsLogger.debug(`Getting credentials for role ${params.roleName} in account ${params.accountId}`); try { // Check if we have valid cached credentials let credentials = await (0, vendor_aws_sso_accounts_service_js_1.getCachedCredentials)(params.accountId, params.roleName); let fromCache = false; if (credentials) { credentialsLogger.debug('Using cached credentials'); fromCache = true; } else { // Get fresh credentials credentialsLogger.debug('Getting fresh credentials'); credentials = await (0, vendor_aws_sso_accounts_service_js_1.getAwsCredentials)({ accountId: params.accountId, roleName: params.roleName, // Vendor implementation doesn't use accessToken parameter directly // It will get the token from the cache }); } // Convert AWS SDK credentials to the format expected by the formatter const convertedCredentials = { accessKeyId: credentials.accessKeyId, secretAccessKey: credentials.secretAccessKey, sessionToken: credentials.sessionToken, expiration: typeof credentials.expiration === 'object' ? credentials.expiration.getTime() / 1000 // Convert Date to unix timestamp : credentials.expiration, region: credentials.region, }; return { content: (0, aws_sso_auth_formatter_js_1.formatCredentials)(fromCache, params.accountId, params.roleName, convertedCredentials), }; } catch (error) { throw (0, error_handler_util_js_1.handleControllerError)(error, (0, error_types_util_js_1.buildErrorContext)('AWS Credentials', 'retrieving', 'controllers/aws.sso.auth.controller.ts@getCredentials', `${params.accountId}/${params.roleName}`, { accountId: params.accountId, roleName: params.roleName, })); } } /** * Check if user is authenticated to AWS SSO * * @returns Promise<{ isAuthenticated: boolean, errorMessage?: string }> */ async function checkSsoAuthStatus() { const statusLogger = logger_util_js_1.Logger.forContext('controllers/aws.sso.auth.controller.ts', 'checkSsoAuthStatus'); statusLogger.debug('Checking AWS SSO authentication status'); try { const token = await (0, vendor_aws_sso_auth_core_service_js_1.getCachedSsoToken)(); if (!token) { statusLogger.debug('No SSO token found'); return { isAuthenticated: false, errorMessage: 'No AWS SSO session found. Please authenticate using login.', }; } // Check if token is expired const now = Math.floor(Date.now() / 1000); // Current time in seconds if (token.expiresAt <= now) { statusLogger.debug('SSO token is expired'); return { isAuthenticated: false, errorMessage: 'AWS SSO session has expired. Please authenticate again using login.', }; } statusLogger.debug('User is authenticated with valid token'); return { isAuthenticated: true }; } catch (error) { statusLogger.error('Error checking authentication status', error); return { isAuthenticated: false, errorMessage: `Error checking authentication: ${error instanceof Error ? error.message : 'Unknown error'}`, }; } } /** * Get the current AWS SSO authentication status * * Checks if the user is currently authenticated to AWS SSO * and returns the status information * * @returns {Promise<ControllerResponse>} Response with authentication status */ async function getAuthStatus() { const statusLogger = logger_util_js_1.Logger.forContext('controllers/aws.sso.auth.controller.ts', 'getAuthStatus'); statusLogger.debug('Getting authentication status'); try { // Use existing checkSsoAuthStatus method const status = await checkSsoAuthStatus(); if (status.isAuthenticated) { // Get token directly to check expiration const token = await (0, vendor_aws_sso_auth_core_service_js_1.getCachedSsoToken)(); // Format expiration date for display let expiresDate = 'Unknown'; try { if (token && token.expiresAt) { const expirationDate = new Date(token.expiresAt * 1000); expiresDate = (0, formatter_util_js_1.formatDate)(expirationDate); } } catch (error) { statusLogger.error('Error formatting expiration date', error); } return { content: (0, aws_sso_auth_formatter_js_1.formatAlreadyLoggedIn)(expiresDate), }; } else { return { content: (0, aws_sso_auth_formatter_js_1.formatAuthRequired)(), }; } } catch (error) { throw (0, error_handler_util_js_1.handleControllerError)(error, (0, error_types_util_js_1.buildErrorContext)('AWS SSO Session', 'checking status', 'controllers/aws.sso.auth.controller.ts@getAuthStatus', undefined, {})); } } exports.default = { startLogin, getCredentials, checkSsoAuthStatus, getAuthStatus, };