azureai-optimizer
Version:
AI-Powered Azure Infrastructure Optimization via Model Context Protocol
156 lines • 6.65 kB
JavaScript
/**
* Azure Authentication Provider
* Handles Azure authentication using DefaultAzureCredential
*/
import { DefaultAzureCredential } from '@azure/identity';
import { SubscriptionClient } from '@azure/arm-subscriptions';
import { Logger } from '../utils/logger.js';
import { MCPError, ErrorCode } from '../utils/errors.js';
export class AzureAuthProvider {
credential;
config;
logger;
authContext = null;
tokenCache = new Map();
constructor(config) {
this.config = config;
this.logger = new Logger('AzureAuth');
// Initialize Azure credential with configuration
const options = {};
if (config.managedIdentityClientId) {
options.managedIdentityClientId = config.managedIdentityClientId;
}
if (config.tenantId) {
options.tenantId = config.tenantId;
}
this.credential = new DefaultAzureCredential(options);
}
async initialize() {
try {
this.logger.info('🔐 Initializing Azure authentication...');
// Test authentication by getting a token
const token = await this.credential.getToken([
'https://management.azure.com/.default'
]);
if (!token) {
throw new MCPError(ErrorCode.UNAUTHORIZED, 'Failed to obtain Azure access token');
}
// Verify subscription access
await this.verifySubscriptionAccess();
// Cache the authentication context
this.authContext = {
credential: this.credential,
subscriptionId: this.config.subscriptionId,
tenantId: this.config.tenantId || 'auto-detected',
accessToken: token.token,
expiresOn: token.expiresOnTimestamp ? new Date(token.expiresOnTimestamp) : new Date(),
permissions: await this.getPermissions()
};
this.logger.info('✅ Azure authentication successful');
this.logger.info(`📋 Subscription: ${this.config.subscriptionId}`);
this.logger.info(`🏢 Tenant: ${this.authContext.tenantId}`);
}
catch (error) {
this.logger.error('❌ Azure authentication failed:', error);
throw new MCPError(ErrorCode.UNAUTHORIZED, `Azure authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async verifySubscriptionAccess() {
try {
const subscriptionClient = new SubscriptionClient(this.credential);
const subscription = await subscriptionClient.subscriptions.get(this.config.subscriptionId);
if (!subscription) {
throw new Error(`Subscription ${this.config.subscriptionId} not found or not accessible`);
}
this.logger.info(`✅ Subscription access verified: ${subscription.displayName}`);
}
catch (error) {
throw new MCPError(ErrorCode.FORBIDDEN, `Cannot access subscription ${this.config.subscriptionId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async getPermissions() {
// This would typically query Azure RBAC to get the user's permissions
// For now, we'll return a basic set based on successful authentication
const permissions = [
'Microsoft.CostManagement/*/read',
'Microsoft.Advisor/*/read',
'Microsoft.Resources/*/read',
'Microsoft.Security/*/read',
'Microsoft.Insights/*/read'
];
this.logger.debug('📋 Detected permissions:', permissions);
return permissions;
}
async getAuthContext() {
if (!this.authContext) {
this.logger.warn('⚠️ Authentication context not available, reinitializing...');
await this.initialize();
}
// Check if token is expired and refresh if needed
if (this.authContext && this.isTokenExpired(this.authContext.expiresOn)) {
this.logger.info('🔄 Access token expired, refreshing...');
await this.refreshToken();
}
return this.authContext;
}
isTokenExpired(expiresOn) {
// Add 5 minute buffer before expiration
const bufferTime = 5 * 60 * 1000; // 5 minutes in milliseconds
return new Date().getTime() > (expiresOn.getTime() - bufferTime);
}
async refreshToken() {
try {
const token = await this.credential.getToken([
'https://management.azure.com/.default'
]);
if (!token || !this.authContext) {
throw new Error('Failed to refresh token');
}
this.authContext.accessToken = token.token;
this.authContext.expiresOn = token.expiresOnTimestamp ? new Date(token.expiresOnTimestamp) : new Date();
this.logger.info('✅ Access token refreshed successfully');
}
catch (error) {
this.logger.error('❌ Failed to refresh access token:', error);
throw new MCPError(ErrorCode.UNAUTHORIZED, 'Failed to refresh Azure access token');
}
}
async getTokenForScope(scope) {
try {
// Check cache first
const cached = this.tokenCache.get(scope);
if (cached && !this.isTokenExpired(cached.expiresOn)) {
return cached.token;
}
// Get new token
const token = await this.credential.getToken([scope]);
if (!token) {
throw new Error(`Failed to get token for scope: ${scope}`);
}
// Cache the token
this.tokenCache.set(scope, {
token: token.token,
expiresOn: token.expiresOnTimestamp ? new Date(token.expiresOnTimestamp) : new Date()
});
return token.token;
}
catch (error) {
this.logger.error(`❌ Failed to get token for scope ${scope}:`, error);
throw new MCPError(ErrorCode.UNAUTHORIZED, `Failed to get token for scope: ${scope}`);
}
}
getCredential() {
return this.credential;
}
getSubscriptionId() {
return this.config.subscriptionId;
}
hasPermission(permission) {
if (!this.authContext) {
return false;
}
// Simple permission check - in production, this would be more sophisticated
return this.authContext.permissions.some(p => p === permission || p.endsWith('/*') && permission.startsWith(p.slice(0, -1)));
}
}
//# sourceMappingURL=azure-auth.js.map