@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
JavaScript
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;
}
}
;