// SECURE: Server-side authorization and session security
const express = require('express');
const session = require('express-session');
const MongoStore = require('connect-mongo');
const crypto = require('crypto');
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');
const app = express();
// SECURE: Security headers and rate limiting
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP'
});
app.use(limiter);
// SECURE: Session configuration
app.use(session({
secret: process.env.SESSION_SECRET || crypto.randomBytes(64).toString('hex'),
name: 'sessionId', // Don't use default session name
resave: false,
saveUninitialized: false,
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI || 'mongodb://localhost/sessions',
touchAfter: 24 * 3600 // lazy session update
}),
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only in production
httpOnly: true, // Prevent XSS
maxAge: 30 * 60 * 1000, // 30 minutes
sameSite: 'strict' // CSRF protection
},
genid: () => {
// Generate cryptographically secure session IDs
return crypto.randomBytes(32).toString('hex');
}
}));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// SECURE: Server-side permission system
class PermissionManager {
constructor() {
this.permissions = new Map();
this.roleHierarchy = {
superadmin: ['admin', 'manager', 'user', 'guest'],
admin: ['manager', 'user', 'guest'],
manager: ['user', 'guest'],
user: ['guest'],
guest: []
};
}
// Define permissions for resources and actions
definePermissions() {
const perms = {
'user:read': ['superadmin', 'admin', 'manager'],
'user:write': ['superadmin', 'admin'],
'user:delete': ['superadmin'],
'user:read-own': ['user'],
'user:write-own': ['user'],
'document:read': ['superadmin', 'admin', 'manager', 'user'],
'document:write': ['superadmin', 'admin', 'manager', 'user'],
'document:delete': ['superadmin', 'admin', 'manager'],
'document:admin': ['superadmin', 'admin'],
'order:read': ['superadmin', 'admin', 'manager'],
'order:write': ['superadmin', 'admin', 'manager'],
'order:read-own': ['user'],
'file:read': ['superadmin', 'admin', 'manager', 'user'],
'file:write': ['superadmin', 'admin', 'manager', 'user'],
'file:delete': ['superadmin', 'admin', 'manager'],
'admin:users': ['superadmin', 'admin'],
'admin:settings': ['superadmin'],
'admin:audit': ['superadmin', 'admin']
};
for (const [permission, roles] of Object.entries(perms)) {
this.permissions.set(permission, new Set(roles));
}
}
// Check if user has permission
hasPermission(userRole, permission) {
const allowedRoles = this.permissions.get(permission);
if (!allowedRoles) {
return false;
}
// Check direct permission
if (allowedRoles.has(userRole)) {
return true;
}
// Check inherited permissions through role hierarchy
const inheritedRoles = this.roleHierarchy[userRole] || [];
return inheritedRoles.some(role => allowedRoles.has(role));
}
// Check resource ownership
async checkOwnership(userId, resourceType, resourceId) {
try {
let resource;
switch (resourceType) {
case 'document':
resource = await Document.findById(resourceId).select('ownerId');
break;
case 'file':
resource = await File.findById(resourceId).select('ownerId');
break;
case 'order':
resource = await Order.findById(resourceId).select('customerId');
return resource && resource.customerId === userId;
default:
return false;
}
return resource && resource.ownerId === userId;
} catch (error) {
console.error('Ownership check error:', error);
return false;
}
}
// Comprehensive permission check
async canPerformAction(user, action, resourceType, resourceId = null) {
if (!user || !user.role) {
return false;
}
const permission = `${resourceType}:${action}`;
const ownPermission = `${resourceType}:${action}-own`;
// Check general permission
if (this.hasPermission(user.role, permission)) {
return true;
}
// Check ownership-based permission
if (resourceId && this.hasPermission(user.role, ownPermission)) {
return await this.checkOwnership(user.id, resourceType, resourceId);
}
return false;
}
}
const permissionManager = new PermissionManager();
permissionManager.definePermissions();
// SECURE: Authentication middleware
function requireAuth(req, res, next) {
if (!req.session || !req.session.user) {
return res.status(401).json({
error: 'Authentication required',
redirectTo: '/login'
});
}
// Validate session integrity
if (!req.session.user.id || !req.session.user.role) {
req.session.destroy();
return res.status(401).json({
error: 'Invalid session',
redirectTo: '/login'
});
}
req.user = req.session.user;
next();
}
// SECURE: Permission middleware factory
function requirePermission(permission) {
return async (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
const hasPermission = permissionManager.hasPermission(req.user.role, permission);
if (!hasPermission) {
// Log unauthorized access attempt
console.warn('Unauthorized access attempt:', {
userId: req.user.id,
userRole: req.user.role,
requiredPermission: permission,
endpoint: req.originalUrl,
method: req.method,
ip: req.ip,
userAgent: req.get('User-Agent'),
timestamp: new Date().toISOString()
});
return res.status(403).json({
error: 'Insufficient permissions',
required: permission
});
}
next();
};
}
// SECURE: Resource-specific authorization middleware
function requireResourceAccess(resourceType, action) {
return async (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
const resourceId = req.params.id || req.params.docId || req.params.fileId;
try {
const canAccess = await permissionManager.canPerformAction(
req.user,
action,
resourceType,
resourceId
);
if (!canAccess) {
console.warn('Resource access denied:', {
userId: req.user.id,
resourceType,
resourceId,
action,
timestamp: new Date().toISOString()
});
return res.status(403).json({
error: 'Access denied to this resource'
});
}
next();
} catch (error) {
console.error('Authorization check error:', error);
res.status(500).json({ error: 'Authorization check failed' });
}
};
}
// SECURE: Login endpoint
app.post('/login', async (req, res) => {
const { username, password } = req.body;
try {
// Input validation
if (!username || !password) {
return res.status(400).json({
error: 'Username and password required'
});
}
// Authenticate user (implement proper password verification)
const user = await User.findOne({ username }).select('+passwordHash');
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
// Log failed login attempt
console.warn('Failed login attempt:', {
username,
ip: req.ip,
userAgent: req.get('User-Agent'),
timestamp: new Date().toISOString()
});
return res.status(401).json({ error: 'Invalid credentials' });
}
// Create secure session
req.session.user = {
id: user.id,
username: user.username,
role: user.role,
loginTime: new Date().toISOString()
};
// Regenerate session ID to prevent session fixation
req.session.regenerate((err) => {
if (err) {
console.error('Session regeneration error:', err);
return res.status(500).json({ error: 'Login failed' });
}
req.session.user = {
id: user.id,
username: user.username,
role: user.role,
loginTime: new Date().toISOString()
};
// Log successful login
console.info('Successful login:', {
userId: user.id,
username: user.username,
ip: req.ip,
timestamp: new Date().toISOString()
});
res.json({
message: 'Login successful',
user: {
id: user.id,
username: user.username,
role: user.role
}
});
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: 'Login failed' });
}
});
// SECURE: Profile endpoint with server-side authorization
app.get('/profile', requireAuth, async (req, res) => {
try {
// SECURE: Always use session user ID, never trust client
const userId = req.user.id;
const user = await User.findById(userId)
.select('username email role createdAt lastLogin preferences');
if (!user) {
req.session.destroy();
return res.status(401).json({ error: 'User not found' });
}
res.json({
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
createdAt: user.createdAt,
lastLogin: user.lastLogin,
preferences: user.preferences
},
session: {
loginTime: req.user.loginTime,
expiresAt: new Date(Date.now() + req.session.cookie.maxAge)
}
});
} catch (error) {
console.error('Profile fetch error:', error);
res.status(500).json({ error: 'Failed to fetch profile' });
}
});
// SECURE: Update profile with proper validation
app.put('/profile', requireAuth, async (req, res) => {
try {
// SECURE: Use session user ID only
const userId = req.user.id;
const { name, email, preferences } = req.body;
// Server-side validation
const updates = {};
if (name) {
if (typeof name !== 'string' || name.length < 1 || name.length > 100) {
return res.status(400).json({
error: 'Name must be 1-100 characters'
});
}
updates.name = name.trim();
}
if (email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return res.status(400).json({
error: 'Invalid email format'
});
}
// Check email uniqueness
const existingUser = await User.findOne({
email: email,
_id: { $ne: userId }
});
if (existingUser) {
return res.status(400).json({
error: 'Email already in use'
});
}
updates.email = email.toLowerCase().trim();
}
if (preferences) {
// Validate preferences object
if (typeof preferences !== 'object') {
return res.status(400).json({
error: 'Invalid preferences format'
});
}
// Sanitize preferences (whitelist allowed fields)
const allowedPrefs = ['theme', 'language', 'notifications', 'timezone'];
const sanitizedPrefs = {};
for (const [key, value] of Object.entries(preferences)) {
if (allowedPrefs.includes(key)) {
sanitizedPrefs[key] = value;
}
}
updates.preferences = sanitizedPrefs;
}
// Update user
const updatedUser = await User.findByIdAndUpdate(
userId,
{ ...updates, updatedAt: new Date() },
{ new: true, select: 'username email name preferences updatedAt' }
);
res.json({
message: 'Profile updated successfully',
user: updatedUser
});
} catch (error) {
console.error('Profile update error:', error);
res.status(500).json({ error: 'Profile update failed' });
}
});
// SECURE: Document access with comprehensive authorization
app.get('/document/:id',
requireAuth,
requireResourceAccess('document', 'read'),
async (req, res) => {
const { id } = req.params;
const userId = req.user.id;
try {
const document = await Document.findById(id)
.populate('ownerId', 'username')
.select('title content ownerId createdAt updatedAt isPublic accessLevel');
if (!document) {
return res.status(404).json({ error: 'Document not found' });
}
// Determine access level for response
const isOwner = document.ownerId._id.toString() === userId;
const hasAdminPermission = permissionManager.hasPermission(req.user.role, 'document:admin');
const response = {
id: document.id,
title: document.title,
content: document.content,
owner: document.ownerId.username,
createdAt: document.createdAt,
isOwner: isOwner,
canEdit: isOwner || hasAdminPermission,
canDelete: isOwner || hasAdminPermission
};
// Log document access
console.info('Document accessed:', {
documentId: id,
userId: userId,
isOwner: isOwner,
timestamp: new Date().toISOString()
});
res.json(response);
} catch (error) {
console.error('Document access error:', error);
res.status(500).json({ error: 'Failed to fetch document' });
}
}
);
// SECURE: Admin user management
app.get('/admin/users',
requireAuth,
requirePermission('admin:users'),
async (req, res) => {
const { page = 1, limit = 50, search } = req.query;
try {
const pageNum = Math.max(1, parseInt(page));
const limitNum = Math.min(100, Math.max(1, parseInt(limit)));
// Build query
let query = {};
if (search) {
query = {
$or: [
{ username: { $regex: search, $options: 'i' } },
{ email: { $regex: search, $options: 'i' } }
]
};
}
const users = await User.find(query)
.select('username email role isActive createdAt lastLogin')
.skip((pageNum - 1) * limitNum)
.limit(limitNum)
.sort({ createdAt: -1 });
const totalUsers = await User.countDocuments(query);
// Log admin access
console.info('Admin users list accessed:', {
adminId: req.user.id,
search: search || 'none',
page: pageNum,
timestamp: new Date().toISOString()
});
res.json({
users: users,
pagination: {
page: pageNum,
limit: limitNum,
total: totalUsers,
pages: Math.ceil(totalUsers / limitNum)
}
});
} catch (error) {
console.error('Admin users fetch error:', error);
res.status(500).json({ error: 'Failed to fetch users' });
}
}
);
// SECURE: Logout with session cleanup
app.post('/logout', requireAuth, (req, res) => {
const userId = req.user.id;
req.session.destroy((err) => {
if (err) {
console.error('Session destruction error:', err);
return res.status(500).json({ error: 'Logout failed' });
}
res.clearCookie('sessionId');
console.info('User logged out:', {
userId: userId,
timestamp: new Date().toISOString()
});
res.json({ message: 'Logged out successfully' });
});
});
// SECURE: Session validation endpoint
app.get('/session/validate', (req, res) => {
if (req.session && req.session.user) {
res.json({
valid: true,
user: {
id: req.session.user.id,
username: req.session.user.username,
role: req.session.user.role
},
expiresAt: new Date(Date.now() + req.session.cookie.maxAge)
});
} else {
res.json({ valid: false });
}
});
// Global error handler
app.use((error, req, res, next) => {
console.error('Unhandled error:', {
error: error.message,
stack: error.stack,
url: req.originalUrl,
method: req.method,
user: req.user?.id,
timestamp: new Date().toISOString()
});
res.status(500).json({
error: 'Internal server error'
});
});
app.listen(3000, () => {
console.log('Secure server running on port 3000');
console.log('Security features enabled:');
console.log('- Server-side authorization');
console.log('- Secure session management');
console.log('- Permission-based access control');
console.log('- Input validation and sanitization');
console.log('- Comprehensive audit logging');
});