HTML & JS Topic

Node.js Authentication & Authorization

1. Password Hashing (bcrypt)

Why Hash Passwords?

  • Security: Protects passwords if database is compromised

  • Irreversible: Cannot be decrypted back to original

  • Salting: Adds random data to prevent rainbow table attacks

Using bcrypt:

bash

npm install bcrypt

Basic Password Hashing:

javascript

// utils/passwordUtils.js
const bcrypt = require('bcrypt');

const passwordUtils = {
  // Hash password
  async hashPassword(password) {
    try {
      // Generate salt (10-12 rounds is recommended)
      const saltRounds = 12;
      const salt = await bcrypt.genSalt(saltRounds);
      
      // Hash password with salt
      const hash = await bcrypt.hash(password, salt);
      return hash;
      
    } catch (error) {
      console.error('Error hashing password:', error);
      throw new Error('Could not hash password');
    }
  },

  // Compare password with hash
  async comparePassword(password, hash) {
    try {
      const isMatch = await bcrypt.compare(password, hash);
      return isMatch;
      
    } catch (error) {
      console.error('Error comparing password:', error);
      throw new Error('Could not verify password');
    }
  },

  // Check password strength
  validatePasswordStrength(password) {
    const requirements = {
      minLength: 8,
      hasUpperCase: /[A-Z]/,
      hasLowerCase: /[a-z]/,
      hasNumbers: /\d/,
      hasSpecialChar: /[!@#$%^&*(),.?":{}|<>]/
    };

    const errors = [];
    
    if (password.length < requirements.minLength) {
      errors.push(`Password must be at least ${requirements.minLength} characters long`);
    }
    if (!requirements.hasUpperCase.test(password)) {
      errors.push('Password must contain at least one uppercase letter');
    }
    if (!requirements.hasLowerCase.test(password)) {
      errors.push('Password must contain at least one lowercase letter');
    }
    if (!requirements.hasNumbers.test(password)) {
      errors.push('Password must contain at least one number');
    }
    if (!requirements.hasSpecialChar.test(password)) {
      errors.push('Password must contain at least one special character');
    }

    return {
      isValid: errors.length === 0,
      errors: errors.length > 0 ? errors : null,
      score: this.calculatePasswordScore(password)
    };
  },

  // Calculate password strength score
  calculatePasswordScore(password) {
    let score = 0;
    
    // Length score
    if (password.length >= 12) score += 4;
    else if (password.length >= 8) score += 2;
    else if (password.length >= 6) score += 1;
    
    // Character variety score
    if (/[a-z]/.test(password)) score += 1;
    if (/[A-Z]/.test(password)) score += 2;
    if (/[0-9]/.test(password)) score += 2;
    if (/[^A-Za-z0-9]/.test(password)) score += 3;
    
    // Deductions for patterns
    if (/(.)\1{2,}/.test(password)) score -= 2; // Repeated characters
    if (/^[0-9]+$/.test(password)) score -= 3; // All numbers
    if (/^[a-zA-Z]+$/.test(password)) score -= 3; // All letters
    
    return Math.max(0, Math.min(score, 10));
  },

  // Generate secure random password
  generateSecurePassword(length = 16) {
    const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?';
    let password = '';
    
    // Ensure at least one character from each category
    password += this.getRandomChar('abcdefghijklmnopqrstuvwxyz'); // lowercase
    password += this.getRandomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ'); // uppercase
    password += this.getRandomChar('0123456789'); // number
    password += this.getRandomChar('!@#$%^&*()_+-=[]{}|;:,.<>?'); // special
    
    // Fill the rest
    for (let i = 4; i < length; i++) {
      password += charset.charAt(Math.floor(Math.random() * charset.length));
    }
    
    // Shuffle the password
    return password.split('').sort(() => Math.random() - 0.5).join('');
  },

  getRandomChar(charset) {
    return charset.charAt(Math.floor(Math.random() * charset.length));
  }
};

module.exports = passwordUtils;

User Model with Password Hashing:

javascript

// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const passwordUtils = require('../utils/passwordUtils');

const userSchema = new mongoose.Schema({
  email: {
    type: String,
    required: true,
    unique: true,
    lowercase: true,
    trim: true
  },
  password: {
    type: String,
    required: true,
    select: false // Don't return password in queries by default
  },
  passwordChangedAt: Date,
  passwordResetToken: String,
  passwordResetExpires: Date,
  failedLoginAttempts: {
    type: Number,
    default: 0
  },
  lockUntil: Date,
  // ... other fields
}, { timestamps: true });

// Middleware to hash password before saving
userSchema.pre('save', async function(next) {
  // Only hash password if it's modified (or new)
  if (!this.isModified('password')) return next();
  
  try {
    // Validate password strength
    const validation = passwordUtils.validatePasswordStrength(this.password);
    if (!validation.isValid) {
      throw new Error(`Password validation failed: ${validation.errors.join(', ')}`);
    }
    
    // Hash password
    this.password = await passwordUtils.hashPassword(this.password);
    this.passwordChangedAt = Date.now() - 1000; // Subtract 1 second for token timing
    next();
  } catch (error) {
    next(error);
  }
});

// Method to compare password
userSchema.methods.comparePassword = async function(candidatePassword) {
  try {
    // If password field is not selected, fetch it
    if (!this.password) {
      const user = await this.model('User').findById(this._id).select('+password');
      return await passwordUtils.comparePassword(candidatePassword, user.password);
    }
    
    return await passwordUtils.comparePassword(candidatePassword, this.password);
  } catch (error) {
    throw error;
  }
};

// Check if password was changed after token was issued
userSchema.methods.changedPasswordAfter = function(JWTTimestamp) {
  if (this.passwordChangedAt) {
    const changedTimestamp = parseInt(this.passwordChangedAt.getTime() / 1000, 10);
    return JWTTimestamp < changedTimestamp;
  }
  return false;
};

// Account lockout for failed attempts
userSchema.methods.incrementLoginAttempts = async function() {
  // Reset lock if expired
  if (this.lockUntil && this.lockUntil < Date.now()) {
    return await this.updateOne({
      $set: { failedLoginAttempts: 1 },
      $unset: { lockUntil: 1 }
    });
  }
  
  const updates = { $inc: { failedLoginAttempts: 1 } };
  
  // Lock account after 5 failed attempts for 1 hour
  if (this.failedLoginAttempts + 1 >= 5 && !this.isLocked) {
    updates.$set = { lockUntil: Date.now() + 60 * 60 * 1000 }; // 1 hour
  }
  
  return await this.updateOne(updates);
};

// Check if account is locked
userSchema.virtual('isLocked').get(function() {
  return !!(this.lockUntil && this.lockUntil > Date.now());
});

// Reset failed login attempts on successful login
userSchema.methods.resetLoginAttempts = async function() {
  return await this.updateOne({
    $set: { failedLoginAttempts: 0 },
    $unset: { lockUntil: 1 }
  });
};

// Generate password reset token
userSchema.methods.createPasswordResetToken = function() {
  const resetToken = crypto.randomBytes(32).toString('hex');
  
  this.passwordResetToken = crypto
    .createHash('sha256')
    .update(resetToken)
    .digest('hex');
    
  this.passwordResetExpires = Date.now() + 10 * 60 * 1000; // 10 minutes
  
  return resetToken;
};

module.exports = mongoose.model('User', userSchema);

2. JWT Authentication

What is JWT?

JSON Web Tokens are compact, URL-safe tokens that contain claims (user data) and are digitally signed.

Structure:

text

Header.Payload.Signature

Installation:

bash

npm install jsonwebtoken

JWT Utils:

javascript

// utils/jwtUtils.js
const jwt = require('jsonwebtoken');
const crypto = require('crypto');

const jwtUtils = {
  // Generate JWT tokens
  generateTokens(payload) {
    const accessToken = jwt.sign(
      payload,
      process.env.JWT_ACCESS_SECRET,
      { expiresIn: process.env.JWT_ACCESS_EXPIRES_IN || '15m' }
    );
    
    const refreshToken = jwt.sign(
      payload,
      process.env.JWT_REFRESH_SECRET,
      { expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d' }
    );
    
    return { accessToken, refreshToken };
  },

  // Verify access token
  verifyAccessToken(token) {
    try {
      return jwt.verify(token, process.env.JWT_ACCESS_SECRET);
    } catch (error) {
      throw new Error('Invalid or expired access token');
    }
  },

  // Verify refresh token
  verifyRefreshToken(token) {
    try {
      return jwt.verify(token, process.env.JWT_REFRESH_SECRET);
    } catch (error) {
      throw new Error('Invalid or expired refresh token');
    }
  },

  // Decode token without verification (for logging, etc.)
  decodeToken(token) {
    return jwt.decode(token);
  },

  // Check if token is about to expire (for proactive refresh)
  isTokenExpiringSoon(token) {
    const decoded = this.decodeToken(token);
    if (!decoded || !decoded.exp) return false;
    
    const now = Math.floor(Date.now() / 1000);
    const timeUntilExpiry = decoded.exp - now;
    
    // Consider token expiring if less than 5 minutes left
    return timeUntilExpiry < 300;
  },

  // Generate token with custom options
  generateTokenWithOptions(payload, options = {}) {
    const defaultOptions = {
      expiresIn: '15m',
      issuer: process.env.JWT_ISSUER || 'your-app',
      audience: process.env.JWT_AUDIENCE || 'your-app-users',
      subject: payload.userId?.toString() || 'unknown'
    };

    const tokenOptions = { ...defaultOptions, ...options };
    
    return jwt.sign(payload, process.env.JWT_ACCESS_SECRET, tokenOptions);
  },

  // Generate secure random key
  generateJWTSecret() {
    return crypto.randomBytes(64).toString('hex');
  },

  // Token blacklist (for logout before expiry)
  blacklistToken: {
    // In production, use Redis for blacklist
    tokens: new Set(),
    
    add(token) {
      const decoded = jwt.decode(token);
      if (decoded && decoded.exp) {
        // Store token until it expires
        this.tokens.add(token);
        
        // Auto-remove after expiry
        const expiryTime = decoded.exp * 1000 - Date.now();
        setTimeout(() => this.remove(token), expiryTime);
      }
    },
    
    remove(token) {
      this.tokens.delete(token);
    },
    
    has(token) {
      return this.tokens.has(token);
    }
  }
};

module.exports = jwtUtils;

Authentication Middleware:

javascript

// middleware/authMiddleware.js
const jwtUtils = require('../utils/jwtUtils');
const User = require('../models/User');

const authMiddleware = {
  // Protect routes - require valid JWT
  protect: async (req, res, next) => {
    try {
      // 1. Get token from header
      let token;
      if (
        req.headers.authorization &&
        req.headers.authorization.startsWith('Bearer')
      ) {
        token = req.headers.authorization.split(' ')[1];
      } else if (req.cookies?.accessToken) {
        token = req.cookies.accessToken;
      }

      if (!token) {
        return res.status(401).json({
          success: false,
          error: 'Not authorized to access this route'
        });
      }

      // 2. Check if token is blacklisted
      if (jwtUtils.blacklistToken.has(token)) {
        return res.status(401).json({
          success: false,
          error: 'Token has been invalidated'
        });
      }

      // 3. Verify token
      const decoded = jwtUtils.verifyAccessToken(token);

      // 4. Check if user still exists
      const user = await User.findById(decoded.userId);
      if (!user) {
        return res.status(401).json({
          success: false,
          error: 'User no longer exists'
        });
      }

      // 5. Check if user changed password after token was issued
      if (user.changedPasswordAfter(decoded.iat)) {
        return res.status(401).json({
          success: false,
          error: 'Password was recently changed. Please login again.'
        });
      }

      // 6. Check if account is locked
      if (user.isLocked) {
        return res.status(401).json({
          success: false,
          error: 'Account is temporarily locked due to too many failed login attempts'
        });
      }

      // 7. Attach user to request
      req.user = user;
      req.token = token;
      next();

    } catch (error) {
      return res.status(401).json({
        success: false,
        error: 'Not authorized to access this route'
      });
    }
  },

  // Optional authentication - user may or may not be authenticated
  optionalAuth: async (req, res, next) => {
    try {
      let token;
      if (
        req.headers.authorization &&
        req.headers.authorization.startsWith('Bearer')
      ) {
        token = req.headers.authorization.split(' ')[1];
      } else if (req.cookies?.accessToken) {
        token = req.cookies.accessToken;
      }

      if (!token) {
        req.user = null;
        return next();
      }

      // Check if token is blacklisted
      if (jwtUtils.blacklistToken.has(token)) {
        req.user = null;
        return next();
      }

      const decoded = jwtUtils.verifyAccessToken(token);
      const user = await User.findById(decoded.userId);

      // Check password change
      if (user && user.changedPasswordAfter(decoded.iat)) {
        req.user = null;
        return next();
      }

      req.user = user || null;
      req.token = token;
      next();

    } catch (error) {
      req.user = null;
      next();
    }
  },

  // Refresh token middleware
  refreshToken: async (req, res, next) => {
    try {
      const refreshToken = req.cookies?.refreshToken || req.body.refreshToken;

      if (!refreshToken) {
        return res.status(401).json({
          success: false,
          error: 'Refresh token required'
        });
      }

      // Verify refresh token
      const decoded = jwtUtils.verifyRefreshToken(refreshToken);

      // Check if user exists
      const user = await User.findById(decoded.userId);
      if (!user) {
        return res.status(401).json({
          success: false,
          error: 'User no longer exists'
        });
      }

      // Check if password changed
      if (user.changedPasswordAfter(decoded.iat)) {
        return res.status(401).json({
          success: false,
          error: 'Password was changed. Please login again.'
        });
      }

      // Generate new tokens
      const tokens = jwtUtils.generateTokens({
        userId: user._id,
        email: user.email,
        role: user.role
      });

      // Attach to request for controller
      req.user = user;
      req.newTokens = tokens;
      next();

    } catch (error) {
      return res.status(401).json({
        success: false,
        error: 'Invalid refresh token'
      });
    }
  }
};

module.exports = authMiddleware;

Auth Controller:

javascript

// controllers/authController.js
const User = require('../models/User');
const jwtUtils = require('../utils/jwtUtils');
const passwordUtils = require('../utils/passwordUtils');
const { sendEmail } = require('../utils/emailService');

const authController = {
  // Register new user
  register: async (req, res) => {
    try {
      const { email, password, name } = req.body;

      // Check if user already exists
      const existingUser = await User.findOne({ email });
      if (existingUser) {
        return res.status(400).json({
          success: false,
          error: 'User with this email already exists'
        });
      }

      // Create user
      const user = await User.create({
        email,
        password,
        name,
        role: 'user' // Default role
      });

      // Generate tokens
      const tokens = jwtUtils.generateTokens({
        userId: user._id,
        email: user.email,
        role: user.role
      });

      // Set cookies (optional)
      res.cookie('accessToken', tokens.accessToken, {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        sameSite: 'strict',
        maxAge: 15 * 60 * 1000 // 15 minutes
      });

      res.cookie('refreshToken', tokens.refreshToken, {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        sameSite: 'strict',
        maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
      });

      // Remove password from response
      user.password = undefined;

      res.status(201).json({
        success: true,
        data: {
          user,
          tokens
        },
        message: 'Registration successful'
      });

    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  },

  // Login
  login: async (req, res) => {
    try {
      const { email, password, rememberMe = false } = req.body;

      // 1. Check if email and password exist
      if (!email || !password) {
        return res.status(400).json({
          success: false,
          error: 'Please provide email and password'
        });
      }

      // 2. Check if user exists && password is correct
      const user = await User.findOne({ email }).select('+password');
      
      if (!user || !(await user.comparePassword(password))) {
        // Increment failed login attempts
        if (user) {
          await user.incrementLoginAttempts();
        }
        
        return res.status(401).json({
          success: false,
          error: 'Invalid email or password'
        });
      }

      // 3. Check if account is locked
      if (user.isLocked) {
        const timeLeft = Math.ceil((user.lockUntil - Date.now()) / (60 * 1000));
        return res.status(401).json({
          success: false,
          error: `Account is locked. Try again in ${timeLeft} minutes`
        });
      }

      // 4. Reset failed login attempts on successful login
      await user.resetLoginAttempts();

      // 5. Update last login
      user.lastLogin = Date.now();
      await user.save();

      // 6. Generate tokens with custom expiration for remember me
      const tokenExpiry = rememberMe ? '30d' : '15m';
      const tokens = jwtUtils.generateTokenWithOptions(
        {
          userId: user._id,
          email: user.email,
          role: user.role
        },
        { expiresIn: tokenExpiry }
      );

      // 7. Set cookies
      const cookieOptions = {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        sameSite: 'strict'
      };

      res.cookie('accessToken', tokens.accessToken, {
        ...cookieOptions,
        maxAge: rememberMe ? 30 * 24 * 60 * 60 * 1000 : 15 * 60 * 1000
      });

      res.cookie('refreshToken', tokens.refreshToken, {
        ...cookieOptions,
        maxAge: 7 * 24 * 60 * 60 * 1000
      });

      // 8. Send response
      user.password = undefined;

      res.json({
        success: true,
        data: {
          user,
          tokens
        },
        message: 'Login successful'
      });

    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  },

  // Logout
  logout: async (req, res) => {
    try {
      // Blacklist the current token
      if (req.token) {
        jwtUtils.blacklistToken.add(req.token);
      }

      // Clear cookies
      res.clearCookie('accessToken');
      res.clearCookie('refreshToken');

      res.json({
        success: true,
        message: 'Logged out successfully'
      });

    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  },

  // Refresh token
  refresh: async (req, res) => {
    try {
      const { newTokens, user } = req;

      // Set new cookies
      res.cookie('accessToken', newTokens.accessToken, {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        sameSite: 'strict',
        maxAge: 15 * 60 * 1000
      });

      res.json({
        success: true,
        data: {
          accessToken: newTokens.accessToken,
          refreshToken: newTokens.refreshToken
        },
        message: 'Token refreshed successfully'
      });

    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  },

  // Forgot password
  forgotPassword: async (req, res) => {
    try {
      const { email } = req.body;

      // 1. Get user based on email
      const user = await User.findOne({ email });
      if (!user) {
        // Don't reveal that user doesn't exist (security)
        return res.json({
          success: true,
          message: 'If an account exists, a reset email has been sent'
        });
      }

      // 2. Generate reset token
      const resetToken = user.createPasswordResetToken();
      await user.save({ validateBeforeSave: false });

      // 3. Send reset email
      const resetURL = `${req.protocol}://${req.get('host')}/api/v1/auth/reset-password/${resetToken}`;
      
      const message = `Forgot your password? Submit a PATCH request with your new password to: ${resetURL}\nIf you didn't forget your password, please ignore this email.`;

      try {
        await sendEmail({
          email: user.email,
          subject: 'Your password reset token (valid for 10 minutes)',
          message
        });

        res.json({
          success: true,
          message: 'Password reset email sent'
        });

      } catch (error) {
        // Reset token if email fails
        user.passwordResetToken = undefined;
        user.passwordResetExpires = undefined;
        await user.save({ validateBeforeSave: false });

        return res.status(500).json({
          success: false,
          error: 'There was an error sending the email. Try again later.'
        });
      }

    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  },

  // Reset password
  resetPassword: async (req, res) => {
    try {
      const { token } = req.params;
      const { password } = req.body;

      // 1. Get user based on token
      const hashedToken = crypto
        .createHash('sha256')
        .update(token)
        .digest('hex');

      const user = await User.findOne({
        passwordResetToken: hashedToken,
        passwordResetExpires: { $gt: Date.now() }
      });

      // 2. If token has not expired, and there is user, set the new password
      if (!user) {
        return res.status(400).json({
          success: false,
          error: 'Token is invalid or has expired'
        });
      }

      // 3. Update password
      user.password = password;
      user.passwordResetToken = undefined;
      user.passwordResetExpires = undefined;
      user.passwordChangedAt = Date.now();
      await user.save();

      // 4. Log the user in, send JWT
      const jwtToken = jwtUtils.generateTokens({
        userId: user._id,
        email: user.email,
        role: user.role
      });

      res.json({
        success: true,
        data: {
          token: jwtToken
        },
        message: 'Password reset successful'
      });

    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  },

  // Update password (when logged in)
  updatePassword: async (req, res) => {
    try {
      const { currentPassword, newPassword } = req.body;
      const user = await User.findById(req.user.id).select('+password');

      // 1. Check if current password is correct
      if (!(await user.comparePassword(currentPassword))) {
        return res.status(401).json({
          success: false,
          error: 'Current password is incorrect'
        });
      }

      // 2. Update password
      user.password = newPassword;
      user.passwordChangedAt = Date.now();
      await user.save();

      // 3. Generate new token
      const token = jwtUtils.generateTokens({
        userId: user._id,
        email: user.email,
        role: user.role
      });

      res.json({
        success: true,
        data: {
          token
        },
        message: 'Password updated successfully'
      });

    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  },

  // Get current user
  getMe: async (req, res) => {
    try {
      const user = await User.findById(req.user.id);
      
      res.json({
        success: true,
        data: {
          user
        }
      });

    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  },

  // Verify email
  verifyEmail: async (req, res) => {
    try {
      const { token } = req.params;
      
      // Find user by verification token
      const user = await User.findOne({
        verificationToken: token,
        verificationTokenExpires: { $gt: Date.now() }
      });

      if (!user) {
        return res.status(400).json({
          success: false,
          error: 'Verification token is invalid or has expired'
        });
      }

      // Mark email as verified
      user.emailVerified = true;
      user.verificationToken = undefined;
      user.verificationTokenExpires = undefined;
      await user.save();

      res.json({
        success: true,
        message: 'Email verified successfully'
      });

    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  }
};

module.exports = authController;

3. Session-based Authentication

Using express-session:

bash

npm install express-session connect-mongo

Session Configuration:

javascript

// config/session.js
const session = require('express-session');
const MongoStore = require('connect-mongo');
const mongoose = require('mongoose');

const sessionConfig = {
  secret: process.env.SESSION_SECRET || 'your-secret-key',
  resave: false,
  saveUninitialized: false,
  store: MongoStore.create({
    mongoUrl: process.env.MONGODB_URI,
    collectionName: 'sessions',
    ttl: 14 * 24 * 60 * 60, // 14 days in seconds
    autoRemove: 'native', // Remove expired sessions
    crypto: {
      secret: process.env.SESSION_ENCRYPTION_SECRET
    }
  }),
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    sameSite: 'strict',
    maxAge: 14 * 24 * 60 * 60 * 1000, // 14 days
    domain: process.env.COOKIE_DOMAIN || 'localhost'
  },
  name: 'sessionId', // Custom session cookie name
  rolling: true, // Reset maxAge on every request
  unset: 'destroy' // Destroy session when unset
};

// Custom session middleware with security
const sessionMiddleware = (req, res, next) => {
  // Skip session for API routes if using JWT
  if (req.path.startsWith('/api') && req.headers.authorization) {
    return next();
  }
  
  session(sessionConfig)(req, res, next);
};

// Session validation middleware
const validateSession = (req, res, next) => {
  if (!req.session.userId) {
    return res.status(401).json({
      success: false,
      error: 'Session expired or invalid'
    });
  }
  
  // Check session age (optional security measure)
  const sessionAge = Date.now() - req.session.cookie._expires;
  if (sessionAge > 7 * 24 * 60 * 60 * 1000) { // Older than 7 days
    req.session.regenerate((err) => {
      if (err) {
        return res.status(500).json({
          success: false,
          error: 'Session regeneration failed'
        });
      }
      next();
    });
  } else {
    next();
  }
};

// Session cleanup for logout
const destroySession = (req, res, next) => {
  req.session.destroy((err) => {
    if (err) {
      return next(err);
    }
    
    res.clearCookie('sessionId');
    next();
  });
};

module.exports = {
  sessionMiddleware,
  validateSession,
  destroySession,
  sessionConfig
};

Session-based Auth Controller:

javascript

// controllers/sessionAuthController.js
const User = require('../models/User');
const passwordUtils = require('../utils/passwordUtils');

const sessionAuthController = {
  // Login with session
  login: async (req, res) => {
    try {
      const { email, password } = req.body;

      // 1. Check if user exists
      const user = await User.findOne({ email }).select('+password');
      
      if (!user || !(await user.comparePassword(password))) {
        // Track failed attempts in session
        req.session.failedAttempts = (req.session.failedAttempts || 0) + 1;
        
        if (req.session.failedAttempts >= 5) {
          req.session.lockUntil = Date.now() + 15 * 60 * 1000; // 15 minutes
        }
        
        return res.status(401).json({
          success: false,
          error: 'Invalid credentials',
          attemptsLeft: 5 - (req.session.failedAttempts || 0)
        });
      }

      // 2. Check if account is locked in session
      if (req.session.lockUntil && req.session.lockUntil > Date.now()) {
        const minutesLeft = Math.ceil((req.session.lockUntil - Date.now()) / (60 * 1000));
        return res.status(401).json({
          success: false,
          error: `Account temporarily locked. Try again in ${minutesLeft} minutes`
        });
      }

      // 3. Create session
      req.session.userId = user._id;
      req.session.userRole = user.role;
      req.session.userEmail = user.email;
      req.session.loginTime = Date.now();
      req.session.userAgent = req.get('User-Agent');
      req.session.ipAddress = req.ip;
      
      // Reset failed attempts
      delete req.session.failedAttempts;
      delete req.session.lockUntil;

      // 4. Update user's last login
      user.lastLogin = Date.now();
      await user.save();

      // 5. Send response
      res.json({
        success: true,
        data: {
          user: {
            id: user._id,
            email: user.email,
            role: user.role
          },
          session: {
            createdAt: req.session.createdAt,
            expires: req.session.cookie.expires
          }
        },
        message: 'Login successful'
      });

    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  },

  // Logout
  logout: (req, res) => {
    req.session.destroy((err) => {
      if (err) {
        return res.status(500).json({
          success: false,
          error: 'Logout failed'
        });
      }
      
      res.clearCookie('sessionId');
      res.json({
        success: true,
        message: 'Logout successful'
      });
    });
  },

  // Get session info
  getSession: (req, res) => {
    if (!req.session.userId) {
      return res.status(401).json({
        success: false,
        error: 'No active session'
      });
    }
    
    res.json({
      success: true,
      data: {
        userId: req.session.userId,
        userRole: req.session.userRole,
        loginTime: req.session.loginTime,
        sessionAge: Date.now() - req.session.loginTime,
        userAgent: req.session.userAgent,
        ipAddress: req.session.ipAddress
      }
    });
  },

  // Change password (session-based)
  changePassword: async (req, res) => {
    try {
      const { currentPassword, newPassword } = req.body;
      
      if (!req.session.userId) {
        return res.status(401).json({
          success: false,
          error: 'Not authenticated'
        });
      }

      const user = await User.findById(req.session.userId).select('+password');
      
      if (!user) {
        // Clear invalid session
        req.session.destroy();
        return res.status(401).json({
          success: false,
          error: 'User not found'
        });
      }

      // Verify current password
      if (!(await user.comparePassword(currentPassword))) {
        return res.status(401).json({
          success: false,
          error: 'Current password is incorrect'
        });
      }

      // Update password
      user.password = newPassword;
      await user.save();

      // Regenerate session to prevent session fixation
      req.session.regenerate((err) => {
        if (err) {
          return res.status(500).json({
            success: false,
            error: 'Session regeneration failed'
          });
        }
        
        // Recreate session with user info
        req.session.userId = user._id;
        req.session.userRole = user.role;
        req.session.userEmail = user.email;
        
        res.json({
          success: true,
          message: 'Password changed successfully'
        });
      });

    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  },

  // Session activity middleware
  trackActivity: (req, res, next) => {
    if (req.session.userId) {
      req.session.lastActivity = Date.now();
      
      // Auto-logout after 30 minutes of inactivity
      if (req.session.lastActivity && 
          Date.now() - req.session.lastActivity > 30 * 60 * 1000) {
        req.session.destroy();
        return res.status(401).json({
          success: false,
          error: 'Session expired due to inactivity'
        });
      }
    }
    next();
  },

  // Multiple sessions management
  getActiveSessions: async (req, res) => {
    try {
      const Session = mongoose.connection.collection('sessions');
      const sessions = await Session.find({
        'session.userId': req.session.userId
      }).toArray();
      
      const activeSessions = sessions.map(session => ({
        id: session._id,
        userAgent: session.session.userAgent,
        ipAddress: session.session.ipAddress,
        loginTime: session.session.loginTime,
        lastActivity: session.session.lastActivity,
        expires: session.expires
      }));
      
      res.json({
        success: true,
        data: {
          sessions: activeSessions,
          count: activeSessions.length
        }
      });
      
    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  },

  // Revoke specific session
  revokeSession: async (req, res) => {
    try {
      const { sessionId } = req.params;
      
      const Session = mongoose.connection.collection('sessions');
      await Session.deleteOne({ _id: new mongoose.Types.ObjectId(sessionId) });
      
      // If revoking current session, logout
      if (req.session.id === sessionId) {
        req.session.destroy();
        res.clearCookie('sessionId');
      }
      
      res.json({
        success: true,
        message: 'Session revoked successfully'
      });
      
    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  },

  // Revoke all other sessions
  revokeOtherSessions: async (req, res) => {
    try {
      const Session = mongoose.connection.collection('sessions');
      await Session.deleteMany({
        'session.userId': req.session.userId,
        _id: { $ne: new mongoose.Types.ObjectId(req.session.id) }
      });
      
      res.json({
        success: true,
        message: 'All other sessions revoked'
      });
      
    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  }
};

module.exports = sessionAuthController;

4. Role-based Access Control (RBAC)

RBAC Implementation:

javascript

// models/Role.js
const mongoose = require('mongoose');

const permissionSchema = new mongoose.Schema({
  resource: {
    type: String,
    required: true,
    enum: ['users', 'products', 'orders', 'categories', 'settings', 'analytics']
  },
  actions: [{
    type: String,
    enum: ['create', 'read', 'update', 'delete', 'manage', 'approve', 'export']
  }],
  conditions: mongoose.Schema.Types.Mixed, // Optional conditions for fine-grained control
  fields: [String] // Specific fields allowed (for partial updates)
}, { _id: false });

const roleSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
    unique: true,
    trim: true
  },
  description: {
    type: String,
    trim: true
  },
  permissions: [permissionSchema],
  isDefault: {
    type: Boolean,
    default: false
  },
  level: {
    type: Number,
    min: 0,
    max: 100,
    default: 0
  },
  inheritsFrom: [{
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Role'
  }],
  createdAt: {
    type: Date,
    default: Date.now
  },
  updatedAt: {
    type: Date,
    default: Date.now
  }
});

// Update timestamps
roleSchema.pre('save', function(next) {
  this.updatedAt = Date.now();
  next();
});

// Get all permissions including inherited ones
roleSchema.methods.getAllPermissions = async function() {
  const permissionsMap = new Map();
  
  // Add direct permissions
  this.permissions.forEach(perm => {
    const key = `${perm.resource}`;
    if (!permissionsMap.has(key)) {
      permissionsMap.set(key, { ...perm });
    } else {
      // Merge actions
      const existing = permissionsMap.get(key);
      existing.actions = [...new Set([...existing.actions, ...perm.actions])];
    }
  });
  
  // Add inherited permissions
  if (this.inheritsFrom && this.inheritsFrom.length > 0) {
    const inheritedRoles = await this.model('Role').find({
      _id: { $in: this.inheritsFrom }
    });
    
    for (const role of inheritedRoles) {
      const inheritedPerms = await role.getAllPermissions();
      inheritedPerms.forEach(perm => {
        const key = `${perm.resource}`;
        if (!permissionsMap.has(key)) {
          permissionsMap.set(key, { ...perm });
        } else {
          const existing = permissionsMap.get(key);
          existing.actions = [...new Set([...existing.actions, ...perm.actions])];
        }
      });
    }
  }
  
  return Array.from(permissionsMap.values());
};

// Check if role has permission
roleSchema.methods.hasPermission = async function(resource, action) {
  const allPermissions = await this.getAllPermissions();
  const permission = allPermissions.find(p => p.resource === resource);
  
  if (!permission) return false;
  
  // Check if action is allowed
  if (permission.actions.includes('manage') || permission.actions.includes('*')) {
    return true;
  }
  
  return permission.actions.includes(action);
};

// Check field-level permission
roleSchema.methods.canAccessField = async function(resource, field) {
  const allPermissions = await this.getAllPermissions();
  const permission = allPermissions.find(p => p.resource === resource);
  
  if (!permission) return false;
  
  // If no fields specified, all fields are accessible
  if (!permission.fields || permission.fields.length === 0) {
    return true;
  }
  
  return permission.fields.includes(field) || permission.fields.includes('*');
};

const Role = mongoose.model('Role', roleSchema);
module.exports = Role;

RBAC Middleware:

javascript

// middleware/rbacMiddleware.js
const User = require('../models/User');
const Role = require('../models/Role');

const rbacMiddleware = {
  // Check if user has specific permission
  hasPermission: (resource, action) => {
    return async (req, res, next) => {
      try {
        if (!req.user) {
          return res.status(401).json({
            success: false,
            error: 'Authentication required'
          });
        }

        // Get user with populated role
        const user = await User.findById(req.user.id).populate('role');
        
        if (!user) {
          return res.status(404).json({
            success: false,
            error: 'User not found'
          });
        }

        // Super admin bypass (optional)
        if (user.isSuperAdmin) {
          return next();
        }

        // Check permission
        const hasPerm = await user.role.hasPermission(resource, action);
        
        if (!hasPerm) {
          return res.status(403).json({
            success: false,
            error: `Insufficient permissions. Required: ${action} on ${resource}`
          });
        }

        next();
      } catch (error) {
        res.status(500).json({
          success: false,
          error: error.message
        });
      }
    };
  },

  // Check permission with conditions
  hasPermissionWithConditions: (resource, action, conditionFn) => {
    return async (req, res, next) => {
      try {
        if (!req.user) {
          return res.status(401).json({
            success: false,
            error: 'Authentication required'
          });
        }

        const user = await User.findById(req.user.id).populate('role');
        
        if (!user) {
          return res.status(404).json({
            success: false,
            error: 'User not found'
          });
        }

        // Check basic permission
        const hasPerm = await user.role.hasPermission(resource, action);
        
        if (!hasPerm) {
          return res.status(403).json({
            success: false,
            error: 'Insufficient permissions'
          });
        }

        // Check additional conditions
        if (conditionFn) {
          const conditionResult = await conditionFn(req, user);
          if (!conditionResult) {
            return res.status(403).json({
              success: false,
              error: 'Access denied by conditions'
            });
          }
        }

        next();
      } catch (error) {
        res.status(500).json({
          success: false,
          error: error.message
        });
      }
    };
  },

  // Field-level access control middleware
  fieldAccess: (resource) => {
    return async (req, res, next) => {
      try {
        if (!req.user) {
          return res.status(401).json({
            success: false,
            error: 'Authentication required'
          });
        }

        const user = await User.findById(req.user.id).populate('role');
        
        // Filter request body based on field permissions
        if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
          const allowedFields = await user.role.getAllowedFields(resource);
          
          if (allowedFields && allowedFields.length > 0 && !allowedFields.includes('*')) {
            Object.keys(req.body).forEach(key => {
              if (!allowedFields.includes(key)) {
                delete req.body[key];
              }
            });
          }
        }

        // Filter response data
        const originalSend = res.send;
        res.send = function(data) {
          if (typeof data === 'object' && data !== null) {
            const filteredData = filterDataByFields(data, allowedFields);
            return originalSend.call(this, filteredData);
          }
          return originalSend.call(this, data);
        };

        next();
      } catch (error) {
        next(error);
      }
    };
  },

  // Role hierarchy check
  hasRoleOrAbove: (requiredRoleLevel) => {
    return async (req, res, next) => {
      try {
        if (!req.user) {
          return res.status(401).json({
            success: false,
            error: 'Authentication required'
          });
        }

        const user = await User.findById(req.user.id).populate('role');
        
        if (!user || !user.role) {
          return res.status(403).json({
            success: false,
            error: 'User role not found'
          });
        }

        if (user.role.level < requiredRoleLevel) {
          return res.status(403).json({
            success: false,
            error: 'Insufficient role level'
          });
        }

        next();
      } catch (error) {
        res.status(500).json({
          success: false,
          error: error.message
        });
      }
    };
  },

  // Multi-role check (user must have at least one of the roles)
  hasAnyRole: (roles) => {
    return async (req, res, next) => {
      try {
        if (!req.user) {
          return res.status(401).json({
            success: false,
            error: 'Authentication required'
          });
        }

        const user = await User.findById(req.user.id).populate('role');
        
        if (!user || !user.role) {
          return res.status(403).json({
            success: false,
            error: 'User role not found'
          });
        }

        if (!roles.includes(user.role.name)) {
          return res.status(403).json({
            success: false,
            error: 'Required role not found'
          });
        }

        next();
      } catch (error) {
        res.status(500).json({
          success: false,
          error: error.message
        });
      }
    };
  }
};

// Helper function to filter data by allowed fields
function filterDataByFields(data, allowedFields) {
  if (!allowedFields || allowedFields.includes('*')) {
    return data;
  }

  if (Array.isArray(data)) {
    return data.map(item => filterObject(item, allowedFields));
  }

  return filterObject(data, allowedFields);
}

function filterObject(obj, allowedFields) {
  const filtered = {};
  allowedFields.forEach(field => {
    if (obj[field] !== undefined) {
      filtered[field] = obj[field];
    }
  });
  return filtered;
}

module.exports = rbacMiddleware;

RBAC Controller:

javascript

// controllers/rbacController.js
const Role = require('../models/Role');
const User = require('../models/User');

const rbacController = {
  // Create new role
  createRole: async (req, res) => {
    try {
      const { name, description, permissions, inheritsFrom, level } = req.body;

      // Check if role already exists
      const existingRole = await Role.findOne({ name });
      if (existingRole) {
        return res.status(400).json({
          success: false,
          error: 'Role already exists'
        });
      }

      // Create role
      const role = await Role.create({
        name,
        description,
        permissions: permissions || [],
        inheritsFrom: inheritsFrom || [],
        level: level || 0
      });

      res.status(201).json({
        success: true,
        data: { role },
        message: 'Role created successfully'
      });

    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  },

  // Get all roles
  getAllRoles: async (req, res) => {
    try {
      const roles = await Role.find().sort({ level: -1, name: 1 });
      
      res.json({
        success: true,
        data: { roles },
        count: roles.length
      });

    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  },

  // Get role by ID
  getRoleById: async (req, res) => {
    try {
      const role = await Role.findById(req.params.id);
      
      if (!role) {
        return res.status(404).json({
          success: false,
          error: 'Role not found'
        });
      }

      // Get all permissions including inherited
      const allPermissions = await role.getAllPermissions();

      res.json({
        success: true,
        data: {
          role,
          allPermissions
        }
      });

    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  },

  // Update role
  updateRole: async (req, res) => {
    try {
      const updates = req.body;
      
      // Prevent updating certain fields
      delete updates._id;
      delete updates.createdAt;

      const role = await Role.findByIdAndUpdate(
        req.params.id,
        { ...updates, updatedAt: Date.now() },
        { new: true, runValidators: true }
      );

      if (!role) {
        return res.status(404).json({
          success: false,
          error: 'Role not found'
        });
      }

      res.json({
        success: true,
        data: { role },
        message: 'Role updated successfully'
      });

    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  },

  // Delete role
  deleteRole: async (req, res) => {
    try {
      // Check if role is in use
      const usersWithRole = await User.countDocuments({ role: req.params.id });
      
      if (usersWithRole > 0) {
        return res.status(400).json({
          success: false,
          error: `Cannot delete role. ${usersWithRole} users are assigned to this role.`
        });
      }

      // Check if other roles inherit from this role
      const inheritingRoles = await Role.countDocuments({ 
        inheritsFrom: req.params.id 
      });
      
      if (inheritingRoles > 0) {
        return res.status(400).json({
          success: false,
          error: `Cannot delete role. ${inheritingRoles} roles inherit from this role.`
        });
      }

      const role = await Role.findByIdAndDelete(req.params.id);

      if (!role) {
        return res.status(404).json({
          success: false,
          error: 'Role not found'
        });
      }

      res.json({
        success: true,
        message: 'Role deleted successfully'
      });

    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  },

  // Assign role to user
  assignRoleToUser: async (req, res) => {
    try {
      const { userId, roleId } = req.body;

      // Check if user exists
      const user = await User.findById(userId);
      if (!user) {
        return res.status(404).json({
          success: false,
          error: 'User not found'
        });
      }

      // Check if role exists
      const role = await Role.findById(roleId);
      if (!role) {
        return res.status(404).json({
          success: false,
          error: 'Role not found'
        });
      }

      // Check if requester has permission to assign this role
      const requesterRole = await Role.findById(req.user.role);
      if (requesterRole.level <= role.level) {
        return res.status(403).json({
          success: false,
          error: 'Cannot assign role of equal or higher level'
        });
      }

      // Assign role
      user.role = roleId;
      await user.save();

      res.json({
        success: true,
        message: `Role '${role.name}' assigned to user successfully`
      });

    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  },

  // Check user permissions
  checkUserPermissions: async (req, res) => {
    try {
      const { userId } = req.params;
      
      const user = await User.findById(userId).populate('role');
      if (!user) {
        return res.status(404).json({
          success: false,
          error: 'User not found'
        });
      }

      const allPermissions = await user.role.getAllPermissions();
      
      res.json({
        success: true,
        data: {
          user: {
            id: user._id,
            email: user.email,
            role: user.role.name
          },
          permissions: allPermissions
        }
      });

    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  },

  // Verify permission for specific action
  verifyPermission: async (req, res) => {
    try {
      const { userId, resource, action } = req.body;
      
      const user = await User.findById(userId).populate('role');
      if (!user) {
        return res.status(404).json({
          success: false,
          error: 'User not found'
        });
      }

      const hasPermission = await user.role.hasPermission(resource, action);
      
      res.json({
        success: true,
        data: {
          hasPermission,
          user: {
            id: user._id,
            role: user.role.name
          },
          resource,
          action
        }
      });

    } catch (error) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  }
};

module.exports = rbacController;

5. OAuth Basics

OAuth 2.0 Implementation:

javascript

// config/oauth.js
const oauthConfig = {
  google: {
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: process.env.GOOGLE_CALLBACK_URL,
    scope: ['profile', 'email'],
    authorizationURL: 'https://accounts.google.com/o/oauth2/v2/auth',
    tokenURL: 'https://oauth2.googleapis.com/token',
    userInfoURL: 'https://www.googleapis.com/oauth2/v3/userinfo'
  },
  
  github: {
    clientID: process.env.GITHUB_CLIENT_ID,
    clientSecret: process.env.GITHUB_CLIENT_SECRET,
    callbackURL: process.env.GITHUB_CALLBACK_URL,
    scope: ['user:email'],
    authorizationURL: 'https://github.com/login/oauth/authorize',
    tokenURL: 'https://github.com/login/oauth/access_token',
    userInfoURL: 'https://api.github.com/user'
  },
  
  facebook: {
    clientID: process.env.FACEBOOK_CLIENT_ID,
    clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
    callbackURL: process.env.FACEBOOK_CALLBACK_URL,
    scope: ['email', 'public_profile'],
    authorizationURL: 'https://www.facebook.com/v12.0/dialog/oauth',
    tokenURL: 'https://graph.facebook.com/v12.0/oauth/access_token',
    userInfoURL: 'https://graph.facebook.com/v12.0/me'
  }
};

module.exports = oauthConfig;

OAuth Service:

javascript

// services/oauthService.js
const axios = require('axios');
const qs = require('querystring');
const crypto = require('crypto');
const User = require('../models/User');
const jwtUtils = require('../utils/jwtUtils');
const oauthConfig = require('../config/oauth');

class OAuthService {
  constructor(provider) {
    this.provider = provider;
    this.config = oauthConfig[provider];
    
    if (!this.config) {
      throw new Error(`Unsupported OAuth provider: ${provider}`);
    }
  }

  // Generate authorization URL
  getAuthorizationUrl(state, additionalParams = {}) {
    const params = {
      client_id: this.config.clientID,
      redirect_uri: this.config.callbackURL,
      response_type: 'code',
      scope: this.config.scope.join(' '),
      state: state,
      ...additionalParams
    };

    return `${this.config.authorizationURL}?${qs.stringify(params)}`;
  }

  // Exchange code for access token
  async getAccessToken(code) {
    try {
      const data = {
        client_id: this.config.clientID,
        client_secret: this.config.clientSecret,
        redirect_uri: this.config.callbackURL,
        code,
        grant_type: 'authorization_code'
      };

      const response = await axios.post(this.config.tokenURL, data, {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          'Accept': 'application/json'
        }
      });

      return response.data;
    } catch (error) {
      console.error('Token exchange error:', error.response?.data || error.message);
      throw new Error('Failed to exchange code for token');
    }
  }

  // Get user info from provider
  async getUserInfo(accessToken) {
    try {
      const response = await axios.get(this.config.userInfoURL, {
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Accept': 'application/json'
        }
      });

      return response.data;
    } catch (error) {
      console.error('User info error:', error.response?.data || error.message);
      throw new Error('Failed to get user info');
    }
  }

  // Process provider-specific user data
  async processUserData(providerData) {
    switch (this.provider) {
      case 'google':
        return {
          email: providerData.email,
          emailVerified: providerData.email_verified || false,
          firstName: providerData.given_name,
          lastName: providerData.family_name,
          avatar: providerData.picture,
          provider: 'google',
          providerId: providerData.sub
        };

      case 'github':
        // GitHub doesn't provide email in user endpoint by default
        let email = providerData.email;
        
        if (!email) {
          // Fetch primary email from GitHub
          const emailResponse = await axios.get('https://api.github.com/user/emails', {
            headers: {
              'Authorization': `Bearer ${accessToken}`,
              'Accept': 'application/json'
            }
          });
          
          const primaryEmail = emailResponse.data.find(e => e.primary);
          email = primaryEmail ? primaryEmail.email : null;
        }

        return {
          email: email,
          emailVerified: true, // GitHub emails are verified
          firstName: providerData.name ? providerData.name.split(' ')[0] : null,
          lastName: providerData.name ? providerData.name.split(' ').slice(1).join(' ') : null,
          avatar: providerData.avatar_url,
          provider: 'github',
          providerId: providerData.id.toString()
        };

      case 'facebook':
        return {
          email: providerData.email,
          emailVerified: true, // Facebook emails are verified
          firstName: providerData.first_name,
          lastName: providerData.last_name,
          avatar: providerData.picture?.data?.url,
          provider: 'facebook',
          providerId: providerData.id
        };

      default:
        throw new Error(`Unsupported provider: ${this.provider}`);
    }
  }

  // Find or create user from OAuth data
  async findOrCreateUser(providerData) {
    try {
      // Look for existing user by provider ID
      let user = await User.findOne({
        provider: this.provider,
        providerId: providerData.providerId
      });

      if (user) {
        // Update last login
        user.lastLogin = Date.now();
        await user.save();
        return user;
      }

      // Look for existing user by email
      if (providerData.email) {
        user = await User.findOne({ email: providerData.email });
        
        if (user) {
          // Link OAuth provider to existing account
          user.provider = this.provider;
          user.providerId = providerData.providerId;
          user.emailVerified = user.emailVerified || providerData.emailVerified;
          user.lastLogin = Date.now();
          await user.save();
          return user;
        }
      }

      // Create new user
      user = await User.create({
        email: providerData.email,
        emailVerified: providerData.emailVerified || false,
        firstName: providerData.firstName,
        lastName: providerData.lastName,
        avatar: providerData.avatar,
        provider: this.provider,
        providerId: providerData.providerId,
        password: crypto.randomBytes(32).toString('hex'), // Random password for OAuth users
        lastLogin: Date.now()
      });

      return user;
    } catch (error) {
      console.error('User creation error:', error);
      throw new Error('Failed to create or find user');
    }
  }

  // Generate state parameter for CSRF protection
  generateState() {
    const state = crypto.randomBytes(32).toString('hex');
    
    // Store state in session or cache with timestamp
    const stateData = {
      state,
      timestamp: Date.now(),
      provider: this.provider
    };
    
    // In production, store in Redis or database
    // For demo, we'll just return it
    return state;
  }

  // Validate state parameter
  validateState(state, storedState) {
    if (!state || !storedState) return false;
    
    // Check if state matches
    if (state !== storedState.state) return false;
    
    // Check if state is not expired (5 minutes)
    const isExpired = Date.now() - storedState.timestamp > 5 * 60 * 1000;
    if (isExpired) return false;
    
    return true;
  }

  // Complete OAuth flow
  async handleCallback(code, state, storedState) {
    try {
      // Validate state
      if (!this.validateState(state, storedState)) {
        throw new Error('Invalid or expired state parameter');
      }

      // Exchange code for token
      const tokenData = await this.getAccessToken(code);
      
      if (!tokenData.access_token) {
        throw new Error('No access token received');
      }

      // Get user info
      const userInfo = await this.getUserInfo(tokenData.access_token);
      
      // Process provider data
      const processedData = await this.processUserData(userInfo);
      
      // Find or create user
      const user = await this.findOrCreateUser(processedData);
      
      // Generate JWT tokens
      const tokens = jwtUtils.generateTokens({
        userId: user._id,
        email: user.email,
        role: user.role
      });

      return {
        user,
        tokens,
        providerData: {
          provider: this.provider,
          accessToken: tokenData.access_token,
          refreshToken: tokenData.refresh_token,
          expiresIn: tokenData.expires_in
        }
      };
    } catch (error) {
      console.error('OAuth callback error:', error);
      throw error;
    }
  }
}

module.exports = OAuthService;

OAuth Routes:

javascript

// routes/oauthRoutes.js
const express = require('express');
const router = express.Router();
const OAuthService = require('../services/oauthService');
const { sessionMiddleware } = require('../config/session');

// Store states in memory (use Redis in production)
const oauthStates = new Map();

// Initiate OAuth flow
router.get('/:provider', (req, res) => {
  try {
    const { provider } = req.params;
    const { redirect } = req.query;
    
    const oauthService = new OAuthService(provider);
    
    // Generate and store state
    const state = oauthService.generateState();
    oauthStates.set(state, {
      state,
      timestamp: Date.now(),
      provider,
      redirect: redirect || '/'
    });
    
    // Store in session if available
    if (req.session) {
      req.session.oauthState = state;
    }
    
    // Redirect to provider's authorization page
    const authUrl = oauthService.getAuthorizationUrl(state);
    res.redirect(authUrl);
    
  } catch (error) {
    res.status(400).json({
      success: false,
      error: error.message
    });
  }
});

// OAuth callback
router.get('/:provider/callback', async (req, res) => {
  try {
    const { provider } = req.params;
    const { code, state, error: providerError } = req.query;
    
    if (providerError) {
      throw new Error(`OAuth provider error: ${providerError}`);
    }
    
    if (!code || !state) {
      throw new Error('Missing code or state parameter');
    }
    
    const oauthService = new OAuthService(provider);
    
    // Get stored state from session or memory
    let storedState;
    if (req.session && req.session.oauthState === state) {
      storedState = { state, timestamp: Date.now(), provider };
    } else {
      storedState = oauthStates.get(state);
    }
    
    if (!storedState) {
      throw new Error('Invalid state parameter');
    }
    
    // Clean up stored state
    oauthStates.delete(state);
    if (req.session) {
      delete req.session.oauthState;
    }
    
    // Handle OAuth callback
    const result = await oauthService.handleCallback(code, state, storedState);
    
    // Set cookies or tokens
    res.cookie('accessToken', result.tokens.accessToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 15 * 60 * 1000
    });
    
    res.cookie('refreshToken', result.tokens.refreshToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000
    });
    
    // Redirect to original destination or success page
    const redirectUrl = storedState.redirect || '/dashboard';
    res.redirect(`${redirectUrl}?oauth_success=true`);
    
  } catch (error) {
    console.error('OAuth callback error:', error);
    res.redirect(`/login?error=${encodeURIComponent(error.message)}`);
  }
});

// Unlink OAuth provider from account
router.post('/:provider/unlink', sessionMiddleware, async (req, res) => {
  try {
    const { provider } = req.params;
    
    if (!req.user) {
      return res.status(401).json({
        success: false,
        error: 'Authentication required'
      });
    }
    
    const user = await User.findById(req.user.id);
    
    if (!user) {
      return res.status(404).json({
        success: false,
        error: 'User not found'
      });
    }
    
    // Check if user has password set (to prevent locking out)
    if (user.provider === provider && !user.password) {
      const hasOtherProvider = await User.findOne({
        _id: { $ne: user._id },
        email: user.email,
        provider: { $ne: provider }
      });
      
      if (!hasOtherProvider) {
        return res.status(400).json({
          success: false,
          error: 'Set a password before unlinking your only OAuth provider'
        });
      }
    }
    
    // Unlink provider
    if (user.provider === provider) {
      user.provider = null;
      user.providerId = null;
      await user.save();
    }
    
    res.json({
      success: true,
      message: `${provider} account unlinked successfully`
    });
    
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
});

// Get connected OAuth providers
router.get('/providers/connected', sessionMiddleware, async (req, res) => {
  try {
    if (!req.user) {
      return res.status(401).json({
        success: false,
        error: 'Authentication required'
      });
    }
    
    const user = await User.findById(req.user.id);
    
    const connectedProviders = [];
    if (user.provider) {
      connectedProviders.push({
        provider: user.provider,
        providerId: user.providerId,
        connectedAt: user.createdAt
      });
    }
    
    res.json({
      success: true,
      data: {
        connectedProviders,
        availableProviders: ['google', 'github', 'facebook']
      }
    });
    
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
});

module.exports = router;

This extensive guide covers all aspects of authentication and authorization in Node.js, including password security with bcrypt, JWT and session-based authentication, role-based access control, and OAuth integration. Each section includes practical implementations with security best practices.

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?