aethercall
Version:
A scalable WebRTC video calling API built with Node.js and OpenVidu
261 lines (223 loc) • 7.7 kB
JavaScript
/**
* 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;