/** * Private Controller API authentication using cookie-based session */ import https from 'https'; import { UnifiAuthenticationError, UnifiNetworkError } from '../errors/UnifiErrors.js'; export interface PrivateAuthConfig { baseUrl: string; username: string; password: string; verifySSL?: boolean; } interface SessionCache { cookie: string; csrfToken?: string; expiresAt: number; } /** * Authentication handler for Private Controller API (proxy/network endpoints) * Uses cookie-based session authentication */ export class PrivateAuth { private config: PrivateAuthConfig; private httpsAgent: https.Agent; private sessionCache: SessionCache | null = null; constructor(config: PrivateAuthConfig) { this.config = { verifySSL: false, ...config, }; this.httpsAgent = new https.Agent({ rejectUnauthorized: this.config.verifySSL ?? false, }); } /** * Get authentication cookie for requests */ async getAuthCookie(): Promise { // Check if we have a valid cached session if (this.sessionCache && this.sessionCache.expiresAt > Date.now() + 60000) { // Session is still valid (with 1 minute buffer) return this.sessionCache.cookie; } // Request new session return this.authenticate(); } /** * Get CSRF token if available */ getCsrfToken(): string | undefined { return this.sessionCache?.csrfToken; } /** * Authenticate and establish session */ private async authenticate(): Promise { const url = `${this.config.baseUrl}/api/auth/login`; try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username: this.config.username, password: this.config.password, }), // @ts-expect-error - agent may not be supported by all fetch implementations agent: this.httpsAgent, }); if (!response.ok) { const errorText = await response.text(); throw new UnifiAuthenticationError( `Failed to authenticate: ${response.status} ${response.statusText} - ${errorText}` ); } // Extract cookie from Set-Cookie header const setCookieHeader = response.headers.get('set-cookie'); if (!setCookieHeader) { throw new UnifiAuthenticationError('No session cookie received from server'); } // Parse cookie (typically format: "csrf_token=...; unifises=...") const cookies = setCookieHeader.split(';').map(c => c.trim()); const sessionCookie = cookies.find(c => c.startsWith('unifises=')) || cookies[0]; if (!sessionCookie) { throw new UnifiAuthenticationError('Invalid session cookie format'); } // Extract CSRF token if present const csrfCookie = cookies.find(c => c.startsWith('csrf_token=')); const csrfToken = csrfCookie ? csrfCookie.split('=')[1] : undefined; // Cache session (default expiration: 1 hour) this.sessionCache = { cookie: sessionCookie, csrfToken, expiresAt: Date.now() + 3600000, // 1 hour }; return sessionCookie; } catch (error) { if (error instanceof UnifiAuthenticationError) { throw error; } throw new UnifiNetworkError( `Failed to connect to UniFi Controller: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined ); } } /** * Clear cached session (force re-authentication on next request) */ clearCredentials(): void { this.sessionCache = null; } /** * Check if we have a valid cached session */ hasValidSession(): boolean { return this.sessionCache !== null && this.sessionCache.expiresAt > Date.now(); } /** * Get HTTPS agent for requests */ getHttpsAgent(): https.Agent { return this.httpsAgent; } }