UNPKG

aethercall

Version:

A scalable WebRTC video calling API built with Node.js and OpenVidu

261 lines (223 loc) 7.7 kB
/** * Token Management Utilities * JWT and OAuth token generation and validation */ const crypto = require('crypto'); class TokenManager { constructor(secret, expiresIn = '24h') { this.secret = secret; this.expiresIn = expiresIn; } /** * Generate a JWT token * @param {Object} payload - Token payload * @param {string} expiresIn - Token expiration time * @returns {string} JWT token */ generateJWT(payload, expiresIn = this.expiresIn) { // Simple JWT implementation (in production, use jsonwebtoken library) const header = { alg: 'HS256', typ: 'JWT' }; const now = Math.floor(Date.now() / 1000); const expiration = this._parseExpiration(expiresIn); const tokenPayload = { ...payload, iat: now, exp: now + expiration }; const encodedHeader = this._base64UrlEncode(JSON.stringify(header)); const encodedPayload = this._base64UrlEncode(JSON.stringify(tokenPayload)); const signature = this._sign(`${encodedHeader}.${encodedPayload}`); return `${encodedHeader}.${encodedPayload}.${signature}`; } /** * Verify and decode a JWT token * @param {string} token - JWT token to verify * @returns {Object} Decoded payload */ verifyJWT(token) { try { const [encodedHeader, encodedPayload, signature] = token.split('.'); if (!encodedHeader || !encodedPayload || !signature) { throw new Error('Invalid token format'); } // Verify signature const expectedSignature = this._sign(`${encodedHeader}.${encodedPayload}`); if (signature !== expectedSignature) { throw new Error('Invalid token signature'); } // Decode payload const payload = JSON.parse(this._base64UrlDecode(encodedPayload)); // Check expiration const now = Math.floor(Date.now() / 1000); if (payload.exp && payload.exp < now) { throw new Error('Token has expired'); } return payload; } catch (error) { throw new Error(`Token verification failed: ${error.message}`); } } /** * Generate a session token for OpenVidu connections * @param {string} sessionId - Session ID * @param {string} userId - User ID * @param {string} role - User role (PUBLISHER, SUBSCRIBER, MODERATOR) * @param {Object} metadata - Additional user metadata * @returns {string} Session token */ generateSessionToken(sessionId, userId, role = 'PUBLISHER', metadata = {}) { const payload = { sessionId, userId, role, metadata, type: 'session', permissions: this._getRolePermissions(role) }; return this.generateJWT(payload); } /** * Generate an API access token * @param {string} clientId - Client ID * @param {Array} scopes - API scopes * @param {Object} metadata - Additional metadata * @returns {string} API token */ generateAPIToken(clientId, scopes = [], metadata = {}) { const payload = { clientId, scopes, metadata, type: 'api' }; return this.generateJWT(payload); } /** * Generate a secure random string * @param {number} length - String length * @returns {string} Random string */ generateSecureRandom(length = 32) { return crypto.randomBytes(length).toString('hex'); } /** * Generate a room access code * @param {number} length - Code length * @returns {string} Room access code */ generateRoomCode(length = 8) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; let result = ''; for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; } /** * Validate session token and extract session info * @param {string} token - Session token * @returns {Object} Session information */ validateSessionToken(token) { const payload = this.verifyJWT(token); if (payload.type !== 'session') { throw new Error('Invalid token type for session'); } return { sessionId: payload.sessionId, userId: payload.userId, role: payload.role, metadata: payload.metadata || {}, permissions: payload.permissions || [] }; } /** * Validate API token and extract API info * @param {string} token - API token * @returns {Object} API information */ validateAPIToken(token) { const payload = this.verifyJWT(token); if (payload.type !== 'api') { throw new Error('Invalid token type for API'); } return { clientId: payload.clientId, scopes: payload.scopes || [], metadata: payload.metadata || {} }; } /** * Get role-based permissions * @param {string} role - User role * @returns {Array} List of permissions */ _getRolePermissions(role) { const permissions = { SUBSCRIBER: ['view', 'listen'], PUBLISHER: ['view', 'listen', 'publish', 'speak'], MODERATOR: ['view', 'listen', 'publish', 'speak', 'kick', 'mute', 'record'] }; return permissions[role] || permissions.SUBSCRIBER; } /** * Parse expiration string to seconds * @param {string} expiresIn - Expiration string (e.g., '24h', '30d', '1m') * @returns {number} Expiration in seconds */ _parseExpiration(expiresIn) { const units = { s: 1, m: 60, h: 3600, d: 86400 }; const match = expiresIn.match(/^(\d+)([smhd])$/); if (!match) { throw new Error('Invalid expiration format'); } const [, value, unit] = match; return parseInt(value) * units[unit]; } /** * Create HMAC signature * @param {string} data - Data to sign * @returns {string} Base64 URL-encoded signature */ _sign(data) { const signature = crypto .createHmac('sha256', this.secret) .update(data) .digest('base64'); return this._base64UrlEncode(signature); } /** * Base64 URL encode * @param {string} str - String to encode * @returns {string} Base64 URL-encoded string */ _base64UrlEncode(str) { return Buffer.from(str) .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); } /** * Base64 URL decode * @param {string} str - String to decode * @returns {string} Decoded string */ _base64UrlDecode(str) { // Add padding if needed str += '='.repeat((4 - str.length % 4) % 4); return Buffer.from( str.replace(/-/g, '+').replace(/_/g, '/'), 'base64' ).toString(); } } module.exports = TokenManager;