@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
321 lines (320 loc) • 15.3 kB
JavaScript
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 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");
/**
* 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 and automatically poll for the token
*
* Initiates the device authorization flow, displays verification instructions,
* and optionally waits for authentication completion.
*
* @async
* @param {Object} [params] - Optional parameters for login
* @param {boolean} [params.autoPoll=true] - Whether to automatically poll for token completion
* @param {boolean} [params.launchBrowser=true] - Whether to automatically launch a browser with the verification URI
* @returns {Promise<ControllerResponse>} Response with login result, including accounts if successful
* @throws {Error} If login initialization fails or polling times out
* @example
* // Start login with automatic polling and browser launch
* const result = await startLogin();
*
* // Start login without automatic polling or browser launch
* const result = await startLogin({ autoPoll: false, 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');
// Always poll for token unless explicitly disabled
const autoPoll = params?.autoPoll !== false;
const launchBrowser = params?.launchBrowser !== false;
try {
// Check if we already have a valid token
const cachedToken = await (0, vendor_aws_sso_auth_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),
metadata: {
alreadyLoggedIn: true,
authenticated: true,
accessToken: cachedToken.accessToken,
},
};
}
// 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)();
// Get the cached device info to retrieve additional properties
const cachedDeviceInfo = await (0, vendor_aws_sso_auth_service_js_1.getCachedDeviceAuthorizationInfo)();
// 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);
// If autoPoll is disabled, just return instructions
if (!autoPoll) {
loginLogger.info('Complete the authentication in your browser.');
loginLogger.info("You can then use 'list-accounts' to verify authentication and view available accounts.");
// Return instructions without automatic polling
return {
content: initialContent +
'\n\nComplete the authentication in your browser. ' +
"You can then use 'list-accounts' to verify authentication and view available accounts.",
metadata: {
deviceAuth: {
deviceCode: deviceAuth.deviceCode,
userCode: deviceAuth.userCode,
interval: deviceAuth.interval,
expiresIn: deviceAuth.expiresIn,
browserLaunched: browserLaunched,
// Include clientId and clientSecret if available from cached device info
...(cachedDeviceInfo
? {
clientId: cachedDeviceInfo.clientId,
clientSecret: cachedDeviceInfo.clientSecret,
}
: {}),
},
},
};
}
// With automatic polling enabled, wait for authentication to complete
loginLogger.debug('Automatic polling enabled, waiting for authentication');
loginLogger.info('Waiting for you to complete authentication in your browser...');
// Now poll for the token - this will continuously poll until success or timeout
try {
const authResult = await (0, vendor_aws_sso_auth_service_js_1.pollForSsoToken)();
loginLogger.debug('Authentication successful, token received', {
expiresAt: authResult.expiresAt,
});
// Format expiration date - handle potential invalid dates
let expiresDate = 'Unknown';
try {
if (authResult.expiresAt) {
// Convert seconds to milliseconds for Date constructor
const expirationDate = new Date(authResult.expiresAt * 1000);
expiresDate = expirationDate.toLocaleString();
}
}
catch (error) {
loginLogger.error('Error formatting expiration date', error);
// Keep default value
}
loginLogger.info('Authentication successful!');
// Return success without trying to list accounts
return {
content: (0, aws_sso_auth_formatter_js_1.formatLoginSuccess)(expiresDate),
metadata: {
authenticated: true,
accessToken: authResult.accessToken,
},
};
}
catch (error) {
loginLogger.error('Error during authentication polling', error);
// Return error indicating polling failed
return {
content: `# Authentication Error\n\nAn error occurred while waiting for authentication: ${error instanceof Error ? error.message : 'Unknown error'}\n\nPlease try again.`,
metadata: {
authenticated: false,
error: error instanceof Error
? error.message
: 'Unknown error',
},
};
}
}
catch (error) {
return (0, error_handler_util_js_1.handleControllerError)(error, {
entityType: 'AWS SSO',
operation: 'login and authentication',
source: 'controllers/aws.sso.auth.controller.ts@startLogin',
});
}
}
/**
* 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),
metadata: {
fromCache,
// Do not include the actual credentials in the response metadata
// for security reasons, only return status
credentialsRetrieved: true,
accountId: params.accountId,
roleName: params.roleName,
// Safely access region from a separate property or use empty string
region: credentials?.region || '',
expiration: credentials.expiration,
},
};
}
catch (error) {
return (0, error_handler_util_js_1.handleControllerError)(error, {
entityType: 'AWS Credentials',
entityId: `${params.accountId}/${params.roleName}`,
operation: 'retrieving',
source: 'controllers/aws.sso.auth.controller.ts@getCredentials',
});
}
}
/**
* 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_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'}`,
};
}
}
exports.default = {
startLogin,
getCredentials,
checkSsoAuthStatus,
};
;