UNPKG

@supernick135/face-scanner-client

Version:

Node.js client library for ZKTeco face scanning devices integration with comprehensive API support

363 lines (308 loc) โ€ข 11.2 kB
#!/usr/bin/env node /** * Secure Authentication Manager * Handles admin login, JWT generation, API key creation and refresh * NO sensitive data is written to files - all kept in memory */ class AuthManager { constructor(config) { this.config = config; this.logger = this.createLogger(); // In-memory token storage (more secure than files) this.tokens = { adminJWT: null, apiKey: null, refreshToken: null, expiresAt: null, keyId: null }; // Admin info from login this.adminInfo = null; this.isAuthenticated = false; this.refreshTimer = null; } createLogger() { return { info: (message, meta = {}) => { const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : ''; console.log(`[INFO] ${message}${metaStr}`); }, error: (message, meta = {}) => { const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : ''; console.error(`[ERROR] ${message}${metaStr}`); }, debug: (message, meta = {}) => { if (this.config.DEBUG_MODE) { const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : ''; console.log(`[DEBUG] ${message}${metaStr}`); } }, warn: (message, meta = {}) => { const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : ''; console.warn(`[WARN] ${message}${metaStr}`); } }; } /** * Complete authentication flow */ async authenticate() { try { this.logger.info('๐Ÿ” Starting authentication flow...'); // Check if API key is already provided in config if (this.config.API_KEY) { this.logger.info('๐Ÿ”‘ Using provided API key'); this.tokens.apiKey = this.config.API_KEY; this.isAuthenticated = true; this.logger.info('โœ… Authentication completed using provided API key'); return; } // Fallback to full authentication flow if no API key provided // Step 1: Admin login await this.performAdminLogin(); // Step 2: Generate JWT token await this.generateJWTToken(); // Step 3: Create service API key await this.createAPIKey(); this.isAuthenticated = true; this.logger.info('โœ… Authentication completed successfully'); // Setup auto-refresh this.setupAutoRefresh(); } catch (error) { this.isAuthenticated = false; this.logger.error('โŒ Authentication failed:', error.message); throw error; } } /** * Step 1: Admin login to get JWT */ async performAdminLogin() { this.logger.debug('๐Ÿ”‘ Attempting admin login...'); const loginUrl = `${this.config.API_BASE_URL}/admin/auth/login`; const response = await fetch(loginUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ username: this.config.ADMIN_USERNAME, password: this.config.ADMIN_PASSWORD }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Admin login failed: ${response.status} ${errorText}`); } const result = await response.json(); this.logger.debug('Login response:', JSON.stringify(result, null, 2)); this.adminInfo = result.admin || result; // Check for access_token at root level or in admin object if (result.access_token) { this.tokens.adminJWT = result.access_token; this.logger.debug('โœ… JWT token obtained from login response'); } else if (result.token) { this.tokens.adminJWT = result.token; this.logger.debug('โœ… JWT token obtained from login response (token field)'); } this.logger.info('โœ… Admin login successful'); } /** * Step 2: Generate JWT token from admin info */ async generateJWTToken() { // Skip if we already have a token from login if (this.tokens.adminJWT) { this.logger.debug('โœ… Using JWT token from login response'); return; } // Use the JWT from admin info if available, otherwise generate our own if (this.adminInfo.access_token) { this.tokens.adminJWT = this.adminInfo.access_token; this.logger.debug('โœ… JWT token from admin info'); } else { try { // Use jsonwebtoken library if available const jwt = require('jsonwebtoken'); const JWT_SECRET = 'xK9mP2nQ8vL4jH6tY3wR5sA7bC1dE0fG9hI2kM4oN6pQ8rT0uV2xW4yZ6aB8cD0e'; const adminPayload = { userId: this.adminInfo.adminId || this.adminInfo.id, clientType: 'admin', permissions: this.adminInfo.permissions || [] }; this.tokens.adminJWT = jwt.sign(adminPayload, JWT_SECRET, { expiresIn: '1h', issuer: 'websocket-server', audience: 'facescan-clients' }); this.logger.debug('โœ… JWT token generated using jsonwebtoken library'); } catch (error) { // Fallback to manual JWT creation const JWT_SECRET = 'xK9mP2nQ8vL4jH6tY3wR5sA7bC1dE0fG9hI2kM4oN6pQ8rT0uV2xW4yZ6aB8cD0e'; const adminPayload = { userId: this.adminInfo.adminId || this.adminInfo.id, clientType: 'admin' }; const header = Buffer.from(JSON.stringify({alg: 'HS256', typ: 'JWT'})).toString('base64url'); const payload = Buffer.from(JSON.stringify(adminPayload)).toString('base64url'); const signature = require('crypto').createHmac('sha256', JWT_SECRET).update(`${header}.${payload}`).digest('base64url'); this.tokens.adminJWT = `${header}.${payload}.${signature}`; this.logger.debug('โœ… JWT token generated manually'); } } } /** * Step 3: Create API key for service */ async createAPIKey() { this.logger.debug('๐Ÿ”‘ Creating service API key...'); const apiKeyUrl = `${this.config.API_BASE_URL}/api/admin/keys/generate`; const keyData = { clientType: this.config.SERVICE_CLIENT_TYPE, schoolId: this.config.SERVICE_SCHOOL_ID, canSelfRefresh: true, rateLimitPerMinute: 1000, metadata: { serviceName: this.config.SERVICE_NAME, createdBy: 'service-client', version: '1.0.0' } }; const response = await fetch(apiKeyUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${this.tokens.adminJWT}`, 'Content-Type': 'application/json' }, body: JSON.stringify(keyData) }); if (!response.ok) { const error = await response.text(); throw new Error(`API key creation failed: ${response.status} ${error}`); } const result = await response.json(); if (this.config.DEBUG_MODE) { this.logger.debug('API Key Creation Response:', result); } // Store tokens securely in memory this.tokens.apiKey = result.apiKey; this.tokens.refreshToken = result.refreshToken; // Default to 1 year expiry if not provided this.tokens.expiresAt = result.expiresAt ? new Date(result.expiresAt) : new Date(Date.now() + 365 * 24 * 60 * 60 * 1000); this.tokens.keyId = result.keyId; this.logger.info('โœ… Service API key created', { keyId: this.tokens.keyId, expiresAt: this.tokens.expiresAt.toISOString(), permissions: result.permissions }); } /** * Get current API key */ getAPIKey() { if (!this.isAuthenticated || !this.tokens.apiKey) { throw new Error('Not authenticated or API key not available'); } return this.tokens.apiKey; } /** * Check if authentication is still valid */ isValid() { return this.isAuthenticated && this.tokens.apiKey && this.tokens.expiresAt && new Date() < this.tokens.expiresAt; } /** * Get authentication status */ getStatus() { return { isAuthenticated: this.isAuthenticated, keyId: this.tokens.keyId, expiresAt: this.tokens.expiresAt ? this.tokens.expiresAt.getTime() : null, adminJWT: this.tokens.adminJWT ? 'Present' : null, apiKey: this.tokens.apiKey ? 'Present' : null }; } /** * Refresh API key using refresh token */ async refreshAPIKey() { try { const refreshUrl = `${this.config.API_BASE_URL}/api/admin/keys/${this.tokens.keyId}/rotate`; const response = await fetch(refreshUrl, { method: 'PUT', headers: { 'Authorization': `Bearer ${this.tokens.adminJWT}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ refreshToken: this.tokens.refreshToken }) }); if (!response.ok) { throw new Error(`Refresh failed: ${response.status}`); } const result = await response.json(); // Update tokens this.tokens.apiKey = result.newApiKey; this.tokens.refreshToken = result.newRefreshToken; this.tokens.expiresAt = new Date(result.expiresAt); this.logger.info('โœ… API key refreshed successfully', { keyId: this.tokens.keyId, newExpiresAt: this.tokens.expiresAt.toISOString() }); } catch (error) { this.logger.error('โŒ API key refresh failed:', error.message); throw error; } } /** * Setup auto-refresh timer */ setupAutoRefresh() { if (!this.tokens.expiresAt) return; const timeUntilRefresh = this.tokens.expiresAt.getTime() - Date.now() - this.config.API_KEY_REFRESH_THRESHOLD; // Clear existing timer if (this.refreshTimer) { clearTimeout(this.refreshTimer); } // Node.js setTimeout has a maximum delay, check if it's within limits const maxTimeout = 2147483647; // Max 32-bit signed integer if (timeUntilRefresh > 0 && timeUntilRefresh < maxTimeout) { this.refreshTimer = setTimeout(() => { this.logger.info('๐Ÿ”„ Auto-refreshing API key...'); this.refreshAPIKey().catch(error => { this.logger.error('โŒ Auto-refresh failed:', error.message); }); }, timeUntilRefresh); } else { this.logger.debug('โฐ Auto-refresh timeout too large, skipping automatic refresh'); } } /** * Clean up resources */ destroy() { // Clear refresh timer if (this.refreshTimer) { clearTimeout(this.refreshTimer); this.refreshTimer = null; } // Clear sensitive data this.tokens = { adminJWT: null, apiKey: null, refreshToken: null, expiresAt: null, keyId: null }; this.adminInfo = null; this.isAuthenticated = false; this.logger.info('๐Ÿงน Auth manager destroyed'); } } module.exports = AuthManager;