Skip to main content
Glama
orneryd

M.I.M.I.R - Multi-agent Intelligent Memory & Insight Repository

by orneryd
authManager.ts12.9 kB
import * as vscode from 'vscode'; /** * Authentication Manager for Mimir VSCode Extension * * Supports three modes: * 1. No Auth Mode (MIMIR_ENABLE_SECURITY=false) * 2. Dev Auth Mode (local username/password or API key) * 3. OAuth Mode (browser-based login, then API key) */ export interface AuthConfig { enabled: boolean; devMode: boolean; devUsername?: string; devPassword?: string; oauthEnabled: boolean; } export interface AuthState { authenticated: boolean; apiKey?: string; username?: string; expiresAt?: string; } export class AuthManager { private context: vscode.ExtensionContext; private baseUrl: string; private authState: AuthState | null = null; private oauthResolver: { resolve: (value: any) => void; state: string } | null = null; private instanceId: string; constructor(context: vscode.ExtensionContext, baseUrl: string) { this.context = context; this.baseUrl = baseUrl; this.instanceId = Math.random().toString(36).substring(7); console.log(`[Auth] AuthManager instance created: ${this.instanceId}`); } /** * Update base URL when configuration changes */ updateBaseUrl(baseUrl: string): void { this.baseUrl = baseUrl; } /** * Check authentication status from server */ async checkAuthStatus(): Promise<AuthConfig> { try { const response = await fetch(`${this.baseUrl}/auth/config`); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const serverConfig: any = await response.json(); // Map server response to our AuthConfig interface // Server returns: {devLoginEnabled: boolean, oauthProviders: array} // We need: {enabled: boolean, devMode: boolean, oauthEnabled: boolean} const enabled = serverConfig.devLoginEnabled || (serverConfig.oauthProviders && serverConfig.oauthProviders.length > 0); const devMode = serverConfig.devLoginEnabled || false; const oauthEnabled = serverConfig.oauthProviders && serverConfig.oauthProviders.length > 0; return { enabled, devMode, oauthEnabled }; } catch (error) { console.error('[Auth] Failed to check auth status:', error); // Default to no auth if server unreachable return { enabled: false, devMode: false, oauthEnabled: false }; } } /** * Get stored authentication state from configuration */ async getAuthState(): Promise<AuthState | null> { if (this.authState) { return this.authState; } // Load from configuration const config = vscode.workspace.getConfiguration('mimir'); const apiKey = config.get<string>('auth.apiKey'); const username = config.get<string>('auth.username'); const expiresAt = config.get<string>('auth.expiresAt'); if (apiKey) { this.authState = { authenticated: true, apiKey, username, expiresAt }; return this.authState; } return null; } /** * Save authentication state to configuration */ private async saveAuthState(state: AuthState): Promise<void> { this.authState = state; const config = vscode.workspace.getConfiguration('mimir'); if (state.apiKey) { await config.update('auth.apiKey', state.apiKey, vscode.ConfigurationTarget.Global); } if (state.username) { await config.update('auth.username', state.username, vscode.ConfigurationTarget.Global); } if (state.expiresAt) { await config.update('auth.expiresAt', state.expiresAt, vscode.ConfigurationTarget.Global); } } /** * Clear authentication state from configuration */ async clearAuthState(): Promise<void> { this.authState = null; const config = vscode.workspace.getConfiguration('mimir'); await config.update('auth.apiKey', undefined, vscode.ConfigurationTarget.Global); await config.update('auth.username', undefined, vscode.ConfigurationTarget.Global); await config.update('auth.expiresAt', undefined, vscode.ConfigurationTarget.Global); } /** * Authenticate with the server * Handles all three modes automatically * Reuses existing valid tokens instead of creating new ones */ async authenticate(): Promise<boolean> { // First, check if we already have a valid cached token const existingState = await this.getAuthState(); if (existingState?.authenticated && existingState.apiKey) { // Check if token is expired if (existingState.expiresAt) { const expiresAt = new Date(existingState.expiresAt); if (expiresAt > new Date()) { console.log('[Auth] Using cached valid token'); return true; } console.log('[Auth] Cached token expired, getting new one'); } else { // No expiration, token is valid indefinitely console.log('[Auth] Using cached token (no expiration)'); return true; } } const config = await this.checkAuthStatus(); // Mode 1: No Auth if (!config.enabled) { console.log('[Auth] Security disabled, no authentication required'); this.authState = { authenticated: true }; return true; } // Mode 2: Dev Auth Mode if (config.devMode) { return await this.authenticateDev(config); } // Mode 3: OAuth Mode if (config.oauthEnabled) { return await this.authenticateOAuth(); } vscode.window.showErrorMessage('Mimir: Unknown authentication configuration'); return false; } /** * Dev Auth Mode: Username/Password from configuration */ private async authenticateDev(config: AuthConfig): Promise<boolean> { // Get credentials from VSCode configuration const workspaceConfig = vscode.workspace.getConfiguration('mimir'); const username = workspaceConfig.get<string>('auth.username'); const password = workspaceConfig.get<string>('auth.password'); if (!username || !password) { vscode.window.showErrorMessage( 'Mimir: Please configure mimir.auth.username and mimir.auth.password in settings', 'Open Settings' ).then(selection => { if (selection === 'Open Settings') { vscode.commands.executeCommand('workbench.action.openSettings', 'mimir.auth'); } }); return false; } return await this.loginWithCredentials(username, password); } /** * Login with username/password using OAuth 2.0 token endpoint (RFC 6749) */ private async loginWithCredentials(username: string, password: string): Promise<boolean> { try { // Use OAuth 2.0 RFC 6749 compliant /auth/token endpoint // grant_type=password (Resource Owner Password Credentials) const response = await fetch(`${this.baseUrl}/auth/token`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ grant_type: 'password', username, password }) }); if (!response.ok) { const error = await response.json().catch(() => ({ error: 'invalid_grant' })) as any; const errorMsg = error.error_description || error.error || 'Login failed'; vscode.window.showErrorMessage(`Mimir: ${errorMsg}`); return false; } const data = await response.json() as any; // Calculate expiration date from expires_in (seconds) const expiresAt = data.expires_in ? new Date(Date.now() + data.expires_in * 1000).toISOString() : undefined; // Save access token as API key await this.saveAuthState({ authenticated: true, apiKey: data.access_token, username, expiresAt }); vscode.window.showInformationMessage(`Mimir: Authenticated as ${username}`); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; vscode.window.showErrorMessage(`Mimir: Authentication failed - ${errorMessage}`); return false; } } /** * OAuth Mode: Browser-based login with automatic callback */ private async authenticateOAuth(): Promise<boolean> { console.log(`[Auth] authenticateOAuth called on instance: ${this.instanceId}`); // Generate state token for CSRF protection const state = Math.random().toString(36).substring(7); // Create promise that will resolve when OAuth callback is received const authPromise = new Promise<{ apiKey: string; username: string } | null>((resolve) => { // Store resolver in instance variable for URI handler to use this.oauthResolver = { resolve, state }; console.log(`[Auth] OAuth resolver set on instance ${this.instanceId} with state: ${state}`); // Timeout after 5 minutes setTimeout(() => { if (this.oauthResolver) { console.log(`[Auth] OAuth resolver timed out on instance ${this.instanceId}`); this.oauthResolver = null; resolve(null); } }, 5 * 60 * 1000); }); // Build auth URL with VSCode redirect const redirectUri = encodeURIComponent(`vscode://mimir.mimir-chat/oauth-callback`); const authUrl = `${this.baseUrl}/auth/oauth/login?vscode_redirect=true&state=${state}&redirect_uri=${redirectUri}`; console.log('[Auth] Opening OAuth login:', authUrl); const opened = await vscode.env.openExternal(vscode.Uri.parse(authUrl)); if (!opened) { vscode.window.showErrorMessage('Mimir: Failed to open browser for authentication'); this.oauthResolver = null; return false; } // Show progress while waiting for OAuth const result = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: 'Mimir: Waiting for OAuth login...', cancellable: true }, async (progress, token) => { token.onCancellationRequested(() => { this.oauthResolver = null; }); return await authPromise; }); if (!result) { vscode.window.showWarningMessage('Mimir: OAuth login cancelled or timed out'); return false; } // Save authentication state await this.saveAuthState({ authenticated: true, apiKey: result.apiKey, username: result.username }); vscode.window.showInformationMessage(`Mimir: Authenticated as ${result.username}`); return true; } /** * Handle OAuth callback from URI * Called by extension.ts URI handler */ async handleOAuthCallback(query: URLSearchParams): Promise<void> { console.log(`[Auth] handleOAuthCallback called on instance: ${this.instanceId}`); console.log(`[Auth] oauthResolver status: ${this.oauthResolver ? 'present' : 'null'}`); if (!this.oauthResolver) { console.error(`[Auth] No OAuth resolver found on instance ${this.instanceId}`); return; } const state = query.get('state'); const accessToken = query.get('access_token'); const username = query.get('username'); const error = query.get('error'); // Verify state matches if (state !== this.oauthResolver.state) { console.error('[Auth] State mismatch in OAuth callback'); this.oauthResolver.resolve(null); this.oauthResolver = null; return; } if (error) { console.error('[Auth] OAuth error:', error); this.oauthResolver.resolve(null); this.oauthResolver = null; return; } if (!accessToken) { console.error('[Auth] No access token in OAuth callback'); this.oauthResolver.resolve(null); this.oauthResolver = null; return; } // Resolve with OAuth access token (stateless - no DB storage) this.oauthResolver.resolve({ apiKey: accessToken, // Store OAuth token in apiKey field username: username || 'OAuth user' }); this.oauthResolver = null; } /** * Verify API key works */ private async verifyApiKey(apiKey: string): Promise<boolean> { try { const response = await fetch(`${this.baseUrl}/api/nodes?limit=1`, { headers: { 'X-API-Key': apiKey } }); return response.ok; } catch (error) { return false; } } /** * Get authentication headers for API requests * Returns OAuth 2.0 RFC 6750 compliant Authorization: Bearer header */ async getAuthHeaders(): Promise<Record<string, string>> { const state = await this.getAuthState(); if (state?.apiKey) { // OAuth 2.0 RFC 6750 compliant header return { 'Authorization': `Bearer ${state.apiKey}` }; } return {}; } /** * Logout and clear authentication */ async logout(): Promise<void> { await this.clearAuthState(); vscode.window.showInformationMessage('Mimir: Logged out successfully'); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/orneryd/Mimir'

If you have feedback or need assistance with the MCP directory API, please join our Discord server