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

332 lines (331 loc) 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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.getAwsSsoConfig = getAwsSsoConfig; exports.startSsoLogin = startSsoLogin; exports.pollForSsoToken = pollForSsoToken; exports.checkSsoAuthStatus = checkSsoAuthStatus; exports.getCachedSsoToken = getCachedSsoToken; exports.getCachedDeviceAuthorizationInfo = getCachedDeviceAuthorizationInfo; const logger_util_js_1 = require("../utils/logger.util.js"); const config_util_js_1 = require("../utils/config.util.js"); const error_util_js_1 = require("../utils/error.util.js"); const ssoCache = __importStar(require("../utils/aws.sso.cache.util.js")); const transport_util_js_1 = require("../utils/transport.util.js"); const logger = logger_util_js_1.Logger.forContext('services/vendor.aws.sso.auth.service.ts'); /** * Make a POST request to a URL with JSON body * @param url The URL to post to * @param data The data to send in the request body * @returns The JSON response */ async function post(url, data) { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), }); if (!response.ok) { throw new Error(`Request failed with status ${response.status}: ${await response.text()}`); } return (await response.json()); } /** * Get AWS SSO configuration from the environment * * Retrieves the AWS SSO start URL and region from the environment variables. * These are required for SSO authentication. * * @returns {AwsSsoConfig} AWS SSO configuration * @throws {Error} If AWS SSO configuration is missing */ async function getAwsSsoConfig() { const methodLogger = logger.forMethod('getAwsSsoConfig'); methodLogger.debug('Getting AWS SSO configuration'); const startUrl = config_util_js_1.config.get('AWS_SSO_START_URL'); // Check AWS_SSO_REGION first, then fallback to AWS_REGION, then default to us-east-1 const region = config_util_js_1.config.get('AWS_SSO_REGION') || config_util_js_1.config.get('AWS_REGION') || 'us-east-1'; if (!startUrl) { const error = (0, error_util_js_1.createAuthMissingError)('AWS_SSO_START_URL environment variable is required'); methodLogger.error('Missing AWS SSO configuration', error); throw error; } methodLogger.debug('AWS SSO configuration retrieved', { startUrl, region, }); return { startUrl, region }; } /** * Start the AWS SSO login process * * Initiates the SSO login flow by registering a client and starting device authorization. * Returns a verification URI and user code that the user must visit to complete authentication. * * @returns {Promise<DeviceAuthorizationResponse>} Login information including verification URI and user code * @throws {Error} If login initialization fails */ async function startSsoLogin() { const methodLogger = logger.forMethod('startSsoLogin'); methodLogger.debug('Starting AWS SSO login process'); try { // First, clean up any existing device authorization to ensure we get fresh codes await ssoCache.clearDeviceAuthorizationInfo(); } catch (error) { methodLogger.debug('Error clearing existing device auth info', error); // Continue even if cleanup fails } // Get SSO configuration const { startUrl, region } = await getAwsSsoConfig(); // Step 1: Register client const registerEndpoint = (0, transport_util_js_1.getSsoOidcEndpoint)(region, '/client/register'); const registerResponse = await post(registerEndpoint, { clientName: 'mcp-aws-sso', clientType: 'public', }); methodLogger.debug('Client registered successfully', { clientId: registerResponse.clientId, }); // Step 2: Start device authorization const authEndpoint = (0, transport_util_js_1.getSsoOidcEndpoint)(region, '/device_authorization'); const authResponse = await post(authEndpoint, { clientId: registerResponse.clientId, clientSecret: registerResponse.clientSecret, startUrl, }); // Log entire response for debugging methodLogger.debug('Device authorization started', { verificationUri: authResponse.verificationUri, verificationUriComplete: authResponse.verificationUriComplete, userCode: authResponse.userCode, expiresIn: authResponse.expiresIn, }); // Store device authorization info in cache for later polling await ssoCache.cacheDeviceAuthorizationInfo({ clientId: registerResponse.clientId, clientSecret: registerResponse.clientSecret, deviceCode: authResponse.deviceCode, expiresIn: authResponse.expiresIn, interval: authResponse.interval, region, }); return authResponse; } /** * Poll for SSO token completion * * Polls the AWS SSO token endpoint to check if the user has completed authentication. * Returns the SSO token if successful. * * @returns {Promise<AwsSsoAuthResult>} SSO token data * @throws {Error} If polling fails or user hasn't completed authentication yet */ async function pollForSsoToken() { const methodLogger = logger.forMethod('pollForSsoToken'); methodLogger.debug('Polling for AWS SSO token'); // Get device authorization info from cache const deviceInfo = await ssoCache.getCachedDeviceAuthorizationInfo(); if (!deviceInfo) { const error = (0, error_util_js_1.createAuthMissingError)('No pending SSO authorization. Please start a new login.'); methodLogger.error('No device authorization info found', error); throw error; } // Extract required info const { clientId, clientSecret, deviceCode, interval = 5, // Default to 5 seconds if not specified expiresIn, region, } = deviceInfo; // Calculate token expiration time const startTime = Date.now(); const expirationTime = startTime + expiresIn * 1000; // Token endpoint const tokenEndpoint = (0, transport_util_js_1.getSsoOidcEndpoint)(region, '/token'); // Polling loop let lastPollTime = 0; while (Date.now() < expirationTime) { // Enforce minimum polling interval const timeSinceLastPoll = Date.now() - lastPollTime; if (timeSinceLastPoll < interval * 1000) { // Wait for remainder of interval const waitTime = interval * 1000 - timeSinceLastPoll; methodLogger.debug(`Waiting ${waitTime}ms before next poll attempt`); await new Promise((resolve) => setTimeout(resolve, waitTime)); } lastPollTime = Date.now(); methodLogger.debug('Polling token endpoint'); try { // Try to get the token const tokenResponse = await post(tokenEndpoint, { clientId, clientSecret, deviceCode, grantType: 'urn:ietf:params:oauth:grant-type:device_code', }); // Process token response - handle both camelCase and snake_case responses // AWS seems to return a mix of these formats across different services const accessToken = tokenResponse.accessToken || tokenResponse.access_token; const refreshToken = tokenResponse.refreshToken || tokenResponse.refresh_token; const tokenType = tokenResponse.tokenType || tokenResponse.token_type; const expiresIn = tokenResponse.expiresIn || tokenResponse.expires_in; if (!accessToken) { throw new Error('No access token in response'); } // Create token expiration time const expiresAt = Math.floor(Date.now() / 1000) + expiresIn; // Create standardized token object const ssoToken = { accessToken: accessToken, expiresAt, region, }; // Cache the token await ssoCache.saveSsoToken({ accessToken: accessToken, expiresAt, region, tokenType: tokenType, retrievedAt: Math.floor(Date.now() / 1000), expiresIn: expiresIn, refreshToken: refreshToken || '', }); methodLogger.debug('Successfully obtained SSO token'); return ssoToken; } catch (error) { // Check for "authorization pending" error // This is normal and expected until the user completes the flow if (error instanceof Error && error.message && error.message.includes('authorization_pending')) { methodLogger.debug('Authorization still pending, will retry after interval'); continue; } // For any other error, log and throw methodLogger.error('Error polling for token', error); throw error; } } // If we reach here, the auth flow timed out const timeoutError = (0, error_util_js_1.createAuthTimeoutError)('SSO authentication flow timed out. Please try again.'); methodLogger.error('Authentication flow timed out', timeoutError); throw timeoutError; } /** * Check if the user is authenticated with AWS SSO * * Verifies if a valid SSO token exists in the cache. * * @returns {Promise<AuthCheckResult>} Authentication status result */ async function checkSsoAuthStatus() { const methodLogger = logger.forMethod('checkSsoAuthStatus'); methodLogger.debug('Checking AWS SSO authentication status'); try { // Get token from cache const token = await ssoCache.getCachedSsoToken(); if (!token) { methodLogger.debug('No token found in cache'); return { isAuthenticated: false, errorMessage: 'No SSO token found. Please login first.', }; } // Check if token is expired const now = Math.floor(Date.now() / 1000); if (token.expiresAt <= now) { methodLogger.debug('Token is expired'); return { isAuthenticated: false, errorMessage: 'SSO token is expired. Please login again.', }; } methodLogger.debug('User is authenticated with valid token'); return { isAuthenticated: true }; } catch (error) { methodLogger.error('Error checking authentication status', error); return { isAuthenticated: false, errorMessage: `Error checking authentication: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Get cached SSO token */ async function getCachedSsoToken() { const methodLogger = logger.forMethod('getCachedSsoToken'); methodLogger.debug('Getting cached SSO token'); try { const token = await ssoCache.getCachedSsoToken(); if (!token) { methodLogger.debug('No token found in cache'); return undefined; } // Convert to auth result format const authResult = { accessToken: token.accessToken, expiresAt: token.expiresAt, region: token.region, }; return authResult; } catch (error) { methodLogger.error('Error getting cached token', error); return undefined; } } /** * Get cached device authorization info * @returns Device authorization info from cache or undefined if not found */ async function getCachedDeviceAuthorizationInfo() { const methodLogger = logger.forMethod('getCachedDeviceAuthorizationInfo'); methodLogger.debug('Getting cached device authorization info'); try { const deviceInfo = await ssoCache.getCachedDeviceAuthorizationInfo(); if (!deviceInfo) { methodLogger.debug('No device authorization info found in cache'); return undefined; } methodLogger.debug('Retrieved device authorization info from cache'); return deviceInfo; } catch (error) { methodLogger.error('Error getting cached device authorization info', error); return undefined; } }