Spaces:
Paused
Paused
Download Services/tokenService.js from imkrish/campverse-backend: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/spaces/imkrish/campverse-backend/resolve/main/Services/tokenService.js
- Command line
-
hf download hf://spaces/imkrish/campverse-backend/Services/tokenService.js
-
curl -L -o tokenService.js https://huggingface.co/spaces/imkrish/campverse-backend/resolve/main/Services/tokenService.js
10.4 kB
| const jwt = require('jsonwebtoken'); | |
| const crypto = require('crypto'); | |
| const Session = require('../Models/Session'); | |
| const LoginHistory = require('../Models/LoginHistory'); | |
| const { getDeviceInfo } = require('./deviceInfoService'); | |
| const { logger } = require('../Middleware/errorHandler'); | |
| const { cacheService } = require('./cacheService'); | |
| /** | |
| * Token Service | |
| * Handles JWT access tokens and refresh tokens | |
| * Implements secure token rotation and session management | |
| */ | |
| // Token configuration | |
| const ACCESS_TOKEN_EXPIRY = '1h'; // 1 hour for access token (Extended from 15m) | |
| const REFRESH_TOKEN_EXPIRY_DAYS = 30; // 30 days for refresh token (Extended from 7d) | |
| /** | |
| * Generate access token (short-lived) | |
| * @param {Object} user - User object with id, roles, name | |
| * @param {string} sessionId - Session ID to include in token for blacklist checking | |
| * @returns {string} JWT access token | |
| */ | |
| function generateAccessToken(user, sessionId = null) { | |
| return jwt.sign( | |
| { | |
| id: user._id || user.id, | |
| roles: user.roles || ['user'], | |
| name: user.name, | |
| email: user.email, | |
| sessionId: sessionId, // Include sessionId for session-based revocation | |
| type: 'access' | |
| }, | |
| process.env.JWT_SECRET, | |
| { | |
| expiresIn: ACCESS_TOKEN_EXPIRY, | |
| issuer: 'campverse', | |
| audience: 'campverse-users', | |
| algorithm: 'HS256' | |
| } | |
| ); | |
| } | |
| /** | |
| * Generate token pair (access + refresh) and create session | |
| * @param {Object} user - User object | |
| * @param {Object} req - Express request for device info | |
| * @param {string} authMethod - Authentication method used | |
| * @returns {Object} { accessToken, refreshToken, session } | |
| */ | |
| async function generateTokenPair(user, req, authMethod = 'email') { | |
| try { | |
| // Get device info | |
| const deviceInfo = await getDeviceInfo(req); | |
| // Create session with refresh token first to get session ID | |
| const { session, refreshToken } = await Session.createSession( | |
| user._id || user.id, | |
| deviceInfo, | |
| REFRESH_TOKEN_EXPIRY_DAYS | |
| ); | |
| // Generate access token with session ID for revocation checking | |
| const accessToken = generateAccessToken(user, session._id.toString()); | |
| // Log successful login | |
| await LoginHistory.logAttempt({ | |
| userId: user._id || user.id, | |
| status: 'success', | |
| authMethod, | |
| deviceInfo, | |
| sessionId: session._id | |
| }); | |
| return { | |
| accessToken, | |
| refreshToken, | |
| session, | |
| expiresIn: 60 * 60 // 1 hour in seconds | |
| }; | |
| } catch (error) { | |
| logger.error('Token generation failed:', error); | |
| throw new Error('Failed to generate authentication tokens'); | |
| } | |
| } | |
| /** | |
| * Refresh access token using refresh token | |
| * Implements token rotation for security | |
| * @param {string} refreshToken - Current refresh token | |
| * @param {Object} req - Express request for device info | |
| * @returns {Object} New token pair or null if invalid | |
| */ | |
| async function refreshAccessToken(refreshToken, req) { | |
| try { | |
| // Find session by refresh token | |
| const session = await Session.findByRefreshToken(refreshToken); | |
| if (!session) { | |
| logger.warn('Refresh failed: Session not found for token'); | |
| return { success: false, error: 'Invalid or expired refresh token' }; | |
| } | |
| if (!session.isValid()) { | |
| logger.warn('Refresh failed: Session is invalid or expired', { id: session._id, expiresAt: session.expiresAt }); | |
| return { success: false, error: 'Session expired' }; | |
| } | |
| const user = session.userId; | |
| if (!user) { | |
| return { success: false, error: 'User not found' }; | |
| } | |
| // Generate new access token with session ID for revocation checking | |
| const accessToken = generateAccessToken(user, session._id.toString()); | |
| // Implement Token Rotation: Generate a new refresh token for this session | |
| const newRefreshToken = crypto.randomBytes(64).toString('hex'); | |
| const hashedNewToken = crypto.createHash('sha256').update(newRefreshToken).digest('hex'); | |
| // Update session with new hashed token and set new expiry | |
| session.refreshToken = hashedNewToken; | |
| session.expiresAt = new Date(Date.now() + REFRESH_TOKEN_EXPIRY_DAYS * 24 * 60 * 60 * 1000); | |
| await session.touch(); | |
| // Do not log refresh token rotations as login history entries | |
| return { | |
| success: true, | |
| accessToken, | |
| newRefreshToken, // Return unhashed token for cookie setting | |
| user: { | |
| id: user._id, | |
| name: user.name, | |
| email: user.email, | |
| roles: user.roles, | |
| isVerified: user.isVerified, | |
| profilePhoto: user.profilePhoto | |
| }, | |
| expiresIn: 60 * 60 | |
| }; | |
| } catch (error) { | |
| logger.error('Token refresh failed:', error); | |
| return { success: false, error: 'Token refresh failed' }; | |
| } | |
| } | |
| /** | |
| * Revoke a specific session (logout from one device) | |
| * Also blacklists associated access tokens to force immediate logout | |
| * @param {string} sessionId - Session ID to revoke | |
| * @param {string} userId - User ID for verification | |
| * @returns {boolean} Success status | |
| */ | |
| async function revokeSession(sessionId, userId) { | |
| try { | |
| const session = await Session.findOne({ _id: sessionId, userId }); | |
| if (!session) { | |
| return { success: false, error: 'Session not found' }; | |
| } | |
| await session.revoke('logout'); | |
| // Blacklist all tokens for this session to force immediate logout | |
| // Store session ID in blacklist with 15 min TTL (same as access token expiry) | |
| await cacheService.set(`session:blacklist:${sessionId}`, true, 15 * 60); | |
| // Also invalidate user's cache to reflect session changes | |
| await cacheService.invalidateUser(userId); | |
| return { success: true }; | |
| } catch (error) { | |
| logger.error('Session revocation failed:', error); | |
| return { success: false, error: 'Failed to revoke session' }; | |
| } | |
| } | |
| /** | |
| * Revoke all sessions for user (logout everywhere) | |
| * Also blacklists all associated access tokens to force immediate logout | |
| * @param {string} userId - User ID | |
| * @param {string} currentSessionId - Optional: keep current session active | |
| * @returns {Object} Result with count of revoked sessions | |
| */ | |
| async function revokeAllSessions(userId, currentSessionId = null) { | |
| try { | |
| // Get all active sessions before revoking to blacklist them | |
| const activeSessions = await Session.find({ | |
| userId, | |
| isActive: true, | |
| ...(currentSessionId ? { _id: { $ne: currentSessionId } } : {}) | |
| }); | |
| let result; | |
| if (currentSessionId) { | |
| result = await Session.revokeAllExcept(userId, currentSessionId, 'security'); | |
| } else { | |
| result = await Session.revokeAll(userId, 'logout'); | |
| } | |
| // Blacklist all revoked sessions to force immediate logout | |
| for (const session of activeSessions) { | |
| await cacheService.set(`session:blacklist:${session._id}`, true, 15 * 60); | |
| } | |
| // Invalidate user's cache to reflect session changes | |
| await cacheService.invalidateUser(userId); | |
| return { success: true, revokedCount: result.modifiedCount }; | |
| } catch (error) { | |
| logger.error('Revoke all sessions failed:', error); | |
| return { success: false, error: 'Failed to revoke sessions' }; | |
| } | |
| } | |
| /** | |
| * Get all active sessions for user | |
| * @param {string} userId - User ID | |
| * @returns {Array} List of active sessions | |
| */ | |
| async function getActiveSessions(userId) { | |
| try { | |
| const sessions = await Session.getActiveSessions(userId); | |
| return sessions.map(s => ({ | |
| id: s._id, | |
| device: s.device, | |
| browser: s.browser, | |
| os: s.os, | |
| location: s.location?.formatted || 'Unknown', | |
| ipAddress: s.ipAddress, | |
| createdAt: s.createdAt, | |
| lastActivity: s.lastActivity, | |
| isCurrent: false // Will be set by caller | |
| })); | |
| } catch (error) { | |
| logger.error('Get active sessions failed:', error); | |
| return []; | |
| } | |
| } | |
| /** | |
| * Get login history for user | |
| * @param {string} userId - User ID | |
| * @param {Object} options - Query options | |
| * @returns {Array} Login history entries | |
| */ | |
| async function getLoginHistory(userId, options = {}) { | |
| try { | |
| const history = await LoginHistory.getHistory(userId, options); | |
| return history.map(h => ({ | |
| id: h._id, | |
| status: h.status, | |
| failReason: h.failReason, | |
| authMethod: h.authMethod, | |
| device: h.device, | |
| browser: h.browser, | |
| os: h.os, | |
| location: h.location?.formatted || 'Unknown', | |
| ipAddress: h.ipAddress, | |
| timestamp: h.timestamp | |
| })); | |
| } catch (error) { | |
| logger.error('Get login history failed:', error); | |
| return []; | |
| } | |
| } | |
| /** | |
| * Log failed login attempt | |
| * @param {string} userId - User ID (if known) | |
| * @param {string} failReason - Reason for failure | |
| * @param {Object} req - Express request | |
| * @param {string} authMethod - Auth method attempted | |
| */ | |
| async function logFailedLogin(userId, failReason, req, authMethod = 'email') { | |
| try { | |
| const deviceInfo = await getDeviceInfo(req); | |
| await LoginHistory.logAttempt({ | |
| userId, | |
| status: 'failed', | |
| failReason, | |
| authMethod, | |
| deviceInfo | |
| }); | |
| } catch (error) { | |
| logger.error('Failed to log login attempt:', error); | |
| } | |
| } | |
| /** | |
| * Check for suspicious login activity | |
| * @param {string} userId - User ID | |
| * @returns {Object} Suspicious activity report | |
| */ | |
| async function checkSuspiciousActivity(userId) { | |
| return LoginHistory.checkSuspiciousActivity(userId); | |
| } | |
| /** | |
| * Get login statistics | |
| * @param {string} userId - User ID | |
| * @param {number} days - Number of days to look back | |
| * @returns {Object} Login statistics | |
| */ | |
| async function getLoginStats(userId, days = 30) { | |
| return LoginHistory.getStats(userId, days); | |
| } | |
| /** | |
| * Verify access token | |
| * @param {string} token - Access token to verify | |
| * @returns {Object} Decoded token payload or null | |
| */ | |
| function verifyAccessToken(token) { | |
| try { | |
| const decoded = jwt.verify(token, process.env.JWT_SECRET, { | |
| algorithms: ['HS256'], | |
| issuer: 'campverse', | |
| audience: 'campverse-users' | |
| }); | |
| if (decoded.type !== 'access') { | |
| return null; | |
| } | |
| return decoded; | |
| } catch (error) { | |
| return null; | |
| } | |
| } | |
| module.exports = { | |
| generateAccessToken, | |
| generateTokenPair, | |
| refreshAccessToken, | |
| revokeSession, | |
| revokeAllSessions, | |
| getActiveSessions, | |
| getLoginHistory, | |
| logFailedLogin, | |
| checkSuspiciousActivity, | |
| getLoginStats, | |
| verifyAccessToken, | |
| ACCESS_TOKEN_EXPIRY, | |
| REFRESH_TOKEN_EXPIRY_DAYS | |
| }; | |