@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
JavaScript
/**
* 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;