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!