import axios, { AxiosInstance } from 'axios';
import { AuthCredentials, AuthToken, AuthCredentialsSchema } from './types.js';
export interface UserManagementInfo {
isKeycloak: boolean;
realm?: string;
authMethod: 'keycloak' | 'cognito';
}
export class UmbrellaDualAuth {
private baseURL: string;
private axiosInstance: AxiosInstance;
private token: string | null = null;
private userApiKey: string | null = null;
private tokenExpiry: number = 0;
private userManagementInfo: UserManagementInfo | null = null;
private userKey: string | null = null;
private availableAccounts: any[] = [];
constructor(baseURL: string) {
this.baseURL = baseURL;
this.axiosInstance = axios.create({
baseURL: this.baseURL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
}
/**
* Detect user management system (Keycloak UM 2.0 vs Cognito Old UM)
*/
async detectUserManagementSystem(username: string): Promise<UserManagementInfo> {
try {
console.error(`[DUAL-AUTH] Detecting user management system for: ${username}`);
// Call the user-realm endpoint to check if user is on Keycloak
const realmResponse = await this.axiosInstance.get(`/user-management/users/user-realm`, {
params: { username: username.toLowerCase() }
});
if (realmResponse.status === 200 && realmResponse.data?.realm) {
console.error(`[DUAL-AUTH] User is on Keycloak UM 2.0, realm: ${realmResponse.data.realm}`);
return {
isKeycloak: true,
realm: realmResponse.data.realm,
authMethod: 'keycloak'
};
} else {
console.error(`[DUAL-AUTH] User is on Cognito Old UM (no realm found)`);
return {
isKeycloak: false,
authMethod: 'cognito'
};
}
} catch (error: any) {
console.error(`[DUAL-AUTH] Realm detection failed, defaulting to Cognito: ${error.message}`);
// Default to old user management if detection fails
return {
isKeycloak: false,
authMethod: 'cognito'
};
}
}
async authenticate(credentials: AuthCredentials): Promise<AuthToken> {
// Validate credentials
const validatedCredentials = AuthCredentialsSchema.parse(credentials);
// First, detect which user management system this user is on
this.userManagementInfo = await this.detectUserManagementSystem(validatedCredentials.username);
try {
let authResponse;
if (this.userManagementInfo.isKeycloak) {
// Use new authentication endpoint for Keycloak users (UM 2.0)
console.error(`[DUAL-AUTH] Authenticating Keycloak user with /authentication/token/generate...`);
try {
authResponse = await this.authenticateKeycloak(validatedCredentials);
} catch (keycloakError: any) {
// Fallback: If Keycloak fails with 500, try Cognito (some migrated accounts may still use Cognito)
if (keycloakError.response?.status === 500) {
console.error(`[DUAL-AUTH] Keycloak failed with 500, trying Cognito fallback...`);
// Update the user management info to reflect we're actually using Cognito
this.userManagementInfo = {
isKeycloak: false,
authMethod: 'cognito'
};
authResponse = await this.authenticateCognito(validatedCredentials);
} else {
throw keycloakError;
}
}
} else {
// Use old signin endpoint for Cognito users (Old UM)
console.error(`[DUAL-AUTH] Authenticating Cognito user with /users/signin...`);
authResponse = await this.authenticateCognito(validatedCredentials);
}
return authResponse;
} catch (error: any) {
console.error(`[DUAL-AUTH] Authentication failed for ${this.userManagementInfo.authMethod}: ${error.message}`);
if (error.response) {
console.error(`[DUAL-AUTH] HTTP Status: ${error.response.status}`);
console.error(`[DUAL-AUTH] Response Data:`, JSON.stringify(error.response.data, null, 2));
}
throw error;
}
}
/**
* Authenticate using Keycloak (User Management 2.0)
* Uses the new /authentication/token/generate endpoint with Basic Auth
*/
private async authenticateKeycloak(credentials: AuthCredentials): Promise<AuthToken> {
// Create Basic Auth header - this is what the authentication endpoint expects!
const basicAuth = Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64');
const response = await this.axiosInstance.post('/authentication/token/generate', {
username: credentials.username,
password: credentials.password
}, {
headers: {
'Authorization': `Basic ${basicAuth}`,
'Content-Type': 'application/json'
}
});
if (response.status === 200 && response.data.Authorization && response.data.apikey) {
this.token = response.data.Authorization;
// Build proper API key for Keycloak users
const tempApiKey = response.data.apikey;
const userKey = tempApiKey.split(':')[0];
const properApiKey = await this.buildProperApiKey(userKey, response.data.Authorization);
this.userApiKey = properApiKey;
this.tokenExpiry = Date.now() + (60 * 60 * 1000);
console.error(`[DUAL-AUTH] Keycloak authentication successful`);
return {
Authorization: response.data.Authorization,
apikey: properApiKey
};
} else {
throw new Error('Invalid response from Keycloak authentication endpoint');
}
}
/**
* Authenticate using Cognito (Old User Management)
* Uses the old /users/signin endpoint
*/
private async authenticateCognito(credentials: AuthCredentials): Promise<AuthToken> {
const response = await this.axiosInstance.post('/users/signin', {
username: credentials.username,
password: credentials.password
});
if (response.status === 200 && response.data.jwtToken) {
this.token = response.data.jwtToken;
// For Cognito, the response format is different
// Extract userKey from JWT token payload
const tokenPayload = JSON.parse(Buffer.from(response.data.jwtToken.split('.')[1], 'base64').toString());
const userKey = tokenPayload.username || tokenPayload['custom:username'] || tokenPayload.sub;
// Build proper API key for Cognito users
const properApiKey = await this.buildProperApiKey(userKey, response.data.jwtToken);
this.userApiKey = properApiKey;
this.tokenExpiry = Date.now() + (60 * 60 * 1000);
console.error(`[DUAL-AUTH] Cognito authentication successful`);
return {
Authorization: response.data.jwtToken,
apikey: properApiKey
};
} else {
throw new Error('Invalid response from Cognito authentication endpoint');
}
}
/**
* Build proper API key format by getting user's account data
*/
private async buildProperApiKey(userKey: string, jwtToken: string): Promise<string> {
try {
// Store userKey for dynamic account switching
this.userKey = userKey;
const tempApiKey = `${userKey}:-1`;
// Use different endpoints for different user management systems
const accountsEndpoint = this.userManagementInfo?.isKeycloak
? '/user-management/accounts' // Keycloak users
: '/users'; // Cognito users
console.error(`[DUAL-AUTH] Getting account data from ${accountsEndpoint} for ${this.userManagementInfo?.authMethod} user`);
const accountsResponse = await this.axiosInstance.get(accountsEndpoint, {
headers: {
'Authorization': jwtToken,
'apikey': tempApiKey,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (accountsResponse.status === 200 && accountsResponse.data) {
let accounts = [];
if (this.userManagementInfo?.isKeycloak) {
// Keycloak: response.data is direct array of accounts
accounts = accountsResponse.data;
} else {
// Cognito: response.data is user object with accounts array
accounts = accountsResponse.data.accounts || [];
}
// Store all available accounts for dynamic switching
this.availableAccounts = accounts;
if (accounts.length > 0) {
// Debug: List all available accounts
console.error(`[DUAL-AUTH] 🔍 Available accounts:`);
accounts.forEach((acc: any, index: number) => {
const accountKey = acc.accountKey || acc.account_key || acc.accountId || acc.account_id || 'N/A';
const accountName = acc.accountName || acc.account_name || acc.name || 'N/A';
console.error(`[DUAL-AUTH] ${index + 1}. Key: ${accountKey}, Name: ${accountName}, Type: ${acc.cloudType || 'N/A'}`);
});
// Look for account with key 9350 (known to have anomaly data)
let targetAccount = accounts.find((acc: any) => {
const accountKey = acc.accountKey || acc.account_key || acc.accountId || acc.account_id;
const keyString = String(accountKey);
console.error(`[DUAL-AUTH] 🔍 Checking account: ${accountKey} (${typeof accountKey}) === '9350'?`);
return keyString === '9350' || accountKey === 9350;
});
// If account 9350 not found, use first account as fallback
if (!targetAccount) {
targetAccount = accounts[0];
console.error(`[DUAL-AUTH] 🔍 Account 9350 not found in available accounts, using: ${targetAccount.accountKey || targetAccount.account_key || targetAccount.accountId || 'unknown'}`);
}
const accountKey = targetAccount.accountKey || targetAccount.account_key || targetAccount.accountId || targetAccount.account_id;
const divisionId = 0;
const properApiKey = `${userKey}:${accountKey}:${divisionId}`;
console.error(`[DUAL-AUTH] Built proper API key for ${this.userManagementInfo?.authMethod}: ${properApiKey}`);
if (accountKey === '9350') {
console.error(`[DUAL-AUTH] ✅ Using account 9350 which has anomaly data!`);
} else {
console.error(`[DUAL-AUTH] ⚠️ Using account ${accountKey} (could not find 9350)`);
}
return properApiKey;
} else {
console.error('[DUAL-AUTH] No accounts found, using temporary API key');
return tempApiKey;
}
} else {
console.error('[DUAL-AUTH] Invalid response from accounts endpoint, using temporary API key');
return tempApiKey;
}
} catch (error: any) {
console.error(`[DUAL-AUTH] Failed to get account data: ${error.message}`);
return `${userKey}:-1`;
}
}
/**
* Get current authentication headers for API requests
*/
getAuthHeaders(): AuthToken {
if (!this.token || !this.userApiKey) {
throw new Error('Not authenticated. Call authenticate() first.');
}
if (Date.now() > this.tokenExpiry) {
throw new Error('Token expired. Please re-authenticate.');
}
return {
Authorization: this.token,
apikey: this.userApiKey
};
}
/**
* Get user management system info
*/
getUserManagementInfo(): UserManagementInfo | null {
return this.userManagementInfo;
}
/**
* Check if the current token is still valid
*/
isAuthenticated(): boolean {
return !!(this.token && this.userApiKey && Date.now() < this.tokenExpiry);
}
/**
* Build API key for specific cloud context
*/
buildCloudContextApiKey(cloudContext: string): string | null {
if (!this.userKey || this.availableAccounts.length === 0) {
console.error('[DUAL-AUTH] Cannot build cloud context API key: missing userKey or accounts');
return null;
}
const normalizedContext = cloudContext.toLowerCase();
let targetAccount = null;
console.error(`[DUAL-AUTH] 🔍 Building API key for cloud context: ${normalizedContext}`);
// Find the appropriate account based on cloud context
if (normalizedContext === 'aws') {
// AWS: Look for account 9350
targetAccount = this.availableAccounts.find((acc: any) => {
const accountKey = acc.accountKey || acc.account_key || acc.accountId || acc.account_id;
const keyString = String(accountKey);
return keyString === '9350' || accountKey === 9350;
});
} else if (normalizedContext === 'gcp') {
// GCP: Look for MasterBilling account (contains "Master", "21112", or "59f88c")
targetAccount = this.availableAccounts.find((acc: any) => {
const accountKey = acc.accountKey || acc.account_key || acc.accountId || acc.account_id;
const accountName = acc.accountName || acc.account_name || acc.name || '';
const keyString = String(accountKey);
return keyString === '21112' ||
accountKey === 21112 ||
accountName.toLowerCase().includes('master') ||
keyString.includes('59f88c');
});
} else if (normalizedContext === 'azure') {
// Azure: Prioritize AzureAmortized (23105) which has more recent data, then fallback to others
targetAccount = this.availableAccounts.find((acc: any) => {
const accountKey = acc.accountKey || acc.account_key || acc.accountId || acc.account_id;
const keyString = String(accountKey);
// First priority: AzureAmortized (has 2025 data)
return keyString === '23105' || accountKey === 23105;
});
// If AzureAmortized not found, look for other Azure accounts
if (!targetAccount) {
targetAccount = this.availableAccounts.find((acc: any) => {
const accountKey = acc.accountKey || acc.account_key || acc.accountId || acc.account_id;
const accountName = acc.accountName || acc.account_name || acc.name || '';
const keyString = String(accountKey);
// Check for other Azure accounts or "Azure" in name
return keyString === '9347' || accountKey === 9347 || // Azure-Pileus
accountName.toLowerCase().includes('azure');
});
}
}
if (!targetAccount) {
console.error(`[DUAL-AUTH] ⚠️ No ${normalizedContext.toUpperCase()} account found, using default AWS account`);
// Fallback to AWS account 9350
targetAccount = this.availableAccounts.find((acc: any) => {
const accountKey = acc.accountKey || acc.account_key || acc.accountId || acc.account_id;
const keyString = String(accountKey);
return keyString === '9350' || accountKey === 9350;
});
}
if (!targetAccount) {
console.error(`[DUAL-AUTH] ⚠️ No suitable account found, using first available account`);
targetAccount = this.availableAccounts[0];
}
const accountKey = targetAccount.accountKey || targetAccount.account_key || targetAccount.accountId || targetAccount.account_id;
const divisionId = 0;
const cloudApiKey = `${this.userKey}:${accountKey}:${divisionId}`;
console.error(`[DUAL-AUTH] ✅ Built ${normalizedContext.toUpperCase()} API key: ${cloudApiKey}`);
return cloudApiKey;
}
buildCustomerApiKey(customerAccountKey: string): string | null {
if (!this.userKey || this.availableAccounts.length === 0) {
console.error('[DUAL-AUTH] Cannot build customer API key: missing userKey or accounts');
return null;
}
console.error(`[DUAL-AUTH] 🔍 Building API key for customer account: ${customerAccountKey}`);
// Find the specific customer account
const targetAccount = this.availableAccounts.find((acc: any) => {
const accountKey = acc.accountKey || acc.account_key || acc.accountId || acc.account_id;
const keyString = String(accountKey);
return keyString === customerAccountKey || accountKey === customerAccountKey;
});
if (!targetAccount) {
console.error(`[DUAL-AUTH] ⚠️ Customer account ${customerAccountKey} not found in available accounts`);
return null;
}
const accountKey = targetAccount.accountKey || targetAccount.account_key || targetAccount.accountId || targetAccount.account_id;
const accountName = targetAccount.accountName || targetAccount.account_name || targetAccount.name || '';
const divisionId = 0;
const customerApiKey = `${this.userKey}:${accountKey}:${divisionId}`;
console.error(`[DUAL-AUTH] ✅ Built customer API key for "${accountName}": ${customerApiKey}`);
return customerApiKey;
}
/**
* Get available accounts (for debugging)
*/
getAvailableAccounts(): any[] {
return this.availableAccounts;
}
/**
* Clear the stored authentication data
*/
clearAuth(): void {
this.token = null;
this.userApiKey = null;
this.tokenExpiry = 0;
this.userManagementInfo = null;
this.userKey = null;
this.availableAccounts = [];
}
}