1. Latar Belakang
Memasuki minggu keempat, hari ketiga masih belajar Express.js dan akan membahas Authentication dan Authorization. Ini adalah sistem keamanan dan hak akses dalam aplikasi:
Analogi Bandara Internasional:
- Authentication (Autentikasi) = Pemeriksaan paspor di imigrasi
- "Siapa kamu? Tunjukkan identitasmu!"
- Buktikan bahwa kamu adalah benar-benar dirimu
- Hasil: Kamu terverifikasi sebagai penumpang sah
- Authorization (Otorisasi) = Boarding pass dan akses lounge
- "Apa yang boleh kamu lakukan?"
- Setelah identitas jelas, tentukan aksesnya
- Hasil: Kamu boleh masuk ke lounge, naik pesawat, dll
Perbedaan Fundamental:
text
┌─────────────────┐ ┌─────────────────┐ │ AUTHENTICATION │ │ AUTHORIZATION │ │ (Login/Logout)│ │ (Permissions) │ ├─────────────────┤ ├─────────────────┤ │ "Siapa kamu?" │ │ "Apa yang bisa │ │ │ │ kamu lakukan?" │ ├─────────────────┤ ├─────────────────┤ │ Identitas User │ │ Hak Akses │ │ Email/Password │ │ Role/Permission│ ├─────────────────┤ ├─────────────────┤ │ Dilakukan sekali│ │ Dilakukan setiap│ │ per sesi │ │ request │ └─────────────────┘ └─────────────────┘ │ │ ▼ ▼ Verified User Authorized Actions
2. Alat dan Bahan
a. Perangkat Lunak
- Node.js & npm - Runtime dan package manager
- Express.js - Framework utama
- jsonwebtoken - Library JWT
- bcryptjs - Hashing password
- crypto - Node.js built-in untuk enkripsi
- express-session - Session management
- Postman - Testing endpoints
- VS Code - Code editor
b. Perangkat Keras
- Laptop/PC dengan spesifikasi standar
3. Pembahasan
3.1 Authentication vs Authorization - Visualisasi
javascript
/** * VISUALISASI AUTHENTICATION vs AUTHORIZATION * * AUTHENTICATION (Login): * ┌─────────────────────────────────────────────────────┐ * │ Request: POST /login │ * │ { │ * │ "email": "user@example.com", │ * │ "password": "********" │ * │ } │ * └─────────────────────────────────────────────────────┘ * │ * ▼ * ┌─────────────────────────────────────────────────────┐ * │ VERIFICATION PROCESS: │ * │ 1. Cek email ada? │ * │ 2. Cek password cocok? │ * │ 3. Akun masih aktif? │ * │ 4. Generate JWT Token │ * └─────────────────────────────────────────────────────┘ * │ * ▼ * ┌─────────────────────────────────────────────────────┐ * │ Response: 200 OK │ * │ { │ * │ "token": "eyJhbGciOiJIUzI1NiIs...", │ * │ "user": { id: 1, name: "John" } │ * │ } │ * └─────────────────────────────────────────────────────┘ * * * AUTHORIZATION (Access Control): * ┌─────────────────────────────────────────────────────┐ * │ Request: GET /admin/users │ * │ Headers: { Authorization: "Bearer <token>" } │ * └─────────────────────────────────────────────────────┘ * │ * ▼ * ┌─────────────────────────────────────────────────────┐ * │ AUTHORIZATION CHECK: │ * │ 1. Verify token valid? │ * │ 2. Extract user role (admin/user/guest) │ * │ 3. Check if role has permission? │ * │ 4. ✅ Allow | ❌ Deny │ * └─────────────────────────────────────────────────────┘ * │ * ▼ * ┌─────────────────────────────────────────────────────┐ * │ Response: 200 OK | 403 Forbidden │ * │ { │ * │ "data": [...] | "error": "Forbidden" │ * │ } │ * └─────────────────────────────────────────────────────┘ */
3.2 Praktik Lengkap: Sistem Authentication & Authorization
Mari kita bangun sistem autentikasi komprehensif dari nol:
bash
# 1. Buat folder project mkdir auth-system cd auth-system # 2. Inisialisasi project npm init -y # 3. Install dependencies npm install express dotenv cors helmet morgan compression npm install jsonwebtoken bcryptjs crypto npm install express-session redis connect-redis npm install joi express-validator npm install -D nodemon # 4. Buat struktur folder mkdir -p src/{models,controllers,routes,middleware,config,services,utils}
A. Config & Environment
src/config/env.js
javascript
/** * ENVIRONMENT CONFIGURATION * Semua konfigurasi environment disimpan di sini */ require('dotenv').config(); module.exports = { // Server NODE_ENV: process.env.NODE_ENV || 'development', PORT: parseInt(process.env.PORT) || 3000, // JWT Configuration JWT: { SECRET: process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-in-production', REFRESH_SECRET: process.env.JWT_REFRESH_SECRET || 'your-refresh-secret-key', ACCESS_EXPIRE: process.env.JWT_ACCESS_EXPIRE || '15m', // 15 minutes REFRESH_EXPIRE: process.env.JWT_REFRESH_EXPIRE || '7d', // 7 days ISSUER: process.env.JWT_ISSUER || 'auth-system', AUDIENCE: process.env.JWT_AUDIENCE || 'auth-system-client' }, // Session Configuration SESSION: { SECRET: process.env.SESSION_SECRET || 'session-secret-key', TTL: parseInt(process.env.SESSION_TTL) || 86400, // 24 hours SECURE: process.env.NODE_ENV === 'production' }, // Redis Configuration (untuk session store) REDIS: { URL: process.env.REDIS_URL || 'redis://localhost:6379', ENABLED: process.env.REDIS_ENABLED === 'true' || false }, // Password Configuration PASSWORD: { SALT_ROUNDS: parseInt(process.env.BCRYPT_SALT_ROUNDS) || 10, MIN_LENGTH: parseInt(process.env.PASSWORD_MIN_LENGTH) || 8, REQUIRE_UPPERCASE: process.env.PASSWORD_REQUIRE_UPPERCASE !== 'false', REQUIRE_LOWERCASE: process.env.PASSWORD_REQUIRE_LOWERCASE !== 'false', REQUIRE_NUMBERS: process.env.PASSWORD_REQUIRE_NUMBERS !== 'false', REQUIRE_SPECIAL: process.env.PASSWORD_REQUIRE_SPECIAL !== 'false' }, // Rate Limiting untuk auth endpoints RATE_LIMIT: { LOGIN: { WINDOW_MS: 15 * 60 * 1000, // 15 minutes MAX_ATTEMPTS: 5 // 5 attempts per window }, REGISTER: { WINDOW_MS: 60 * 60 * 1000, // 1 hour MAX_ATTEMPTS: 3 // 3 registrations per hour } } };
B. Models untuk Authentication
src/models/User.model.js
javascript
/** * USER MODEL * Representasi data user dan method autentikasi */ const bcrypt = require('bcryptjs'); const crypto = require('crypto'); const config = require('../config/env'); class User { constructor(userData = {}) { this.id = userData.id || null; this.email = userData.email || ''; this.password = userData.password || ''; this.name = userData.name || ''; this.role = userData.role || 'user'; this.permissions = userData.permissions || []; this.isActive = userData.isActive !== undefined ? userData.isActive : true; this.isEmailVerified = userData.isEmailVerified || false; this.emailVerificationToken = userData.emailVerificationToken || null; this.passwordResetToken = userData.passwordResetToken || null; this.passwordResetExpires = userData.passwordResetExpires || null; this.loginAttempts = userData.loginAttempts || 0; this.lockUntil = userData.lockUntil || null; this.lastLogin = userData.lastLogin || null; this.createdAt = userData.createdAt || new Date(); this.updatedAt = userData.updatedAt || new Date(); this.refreshTokens = userData.refreshTokens || []; } /** * Hash password sebelum disimpan */ static async hashPassword(password) { return await bcrypt.hash(password, config.PASSWORD.SALT_ROUNDS); } /** * Validasi password */ async validatePassword(password) { return await bcrypt.compare(password, this.password); } /** * Generate email verification token */ generateEmailVerificationToken() { const token = crypto.randomBytes(32).toString('hex'); this.emailVerificationToken = crypto .createHash('sha256') .update(token) .digest('hex'); return token; } /** * Verify email token */ verifyEmailToken(token) { const hashedToken = crypto .createHash('sha256') .update(token) .digest('hex'); return this.emailVerificationToken === hashedToken; } /** * Generate password reset token */ generatePasswordResetToken() { const token = crypto.randomBytes(32).toString('hex'); this.passwordResetToken = crypto .createHash('sha256') .update(token) .digest('hex'); this.passwordResetExpires = Date.now() + 3600000; // 1 hour return token; } /** * Verify password reset token */ verifyPasswordResetToken(token) { const hashedToken = crypto .createHash('sha256') .update(token) .digest('hex'); return this.passwordResetToken === hashedToken && this.passwordResetExpires > Date.now(); } /** * Increment login attempts */ incrementLoginAttempts() { this.loginAttempts += 1; // Lock account after 5 failed attempts if (this.loginAttempts >= 5) { this.lockUntil = Date.now() + 30 * 60 * 1000; // 30 minutes } } /** * Reset login attempts on successful login */ resetLoginAttempts() { this.loginAttempts = 0; this.lockUntil = null; this.lastLogin = new Date(); } /** * Check if account is locked */ isLocked() { return this.lockUntil && this.lockUntil > Date.now(); } /** * Add refresh token */ addRefreshToken(token, expiresIn = '7d') { const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + 7); // 7 days this.refreshTokens.push({ token, expiresAt, createdAt: new Date() }); // Keep only last 5 refresh tokens if (this.refreshTokens.length > 5) { this.refreshTokens = this.refreshTokens.slice(-5); } } /** * Revoke refresh token */ revokeRefreshToken(token) { this.refreshTokens = this.refreshTokens.filter(t => t.token !== token); } /** * Check if refresh token is valid */ isValidRefreshToken(token) { const refreshToken = this.refreshTokens.find(t => t.token === token); if (!refreshToken) return false; if (refreshToken.expiresAt < new Date()) return false; return true; } /** * Remove sensitive data before sending to client */ toJSON() { return { id: this.id, email: this.email, name: this.name, role: this.role, permissions: this.permissions, isActive: this.isActive, isEmailVerified: this.isEmailVerified, lastLogin: this.lastLogin, createdAt: this.createdAt, updatedAt: this.updatedAt }; } } /** * DATABASE SIMULASI * Dalam produksi, ini akan diganti dengan MongoDB/PostgreSQL */ const users = []; let nextId = 1; // Seed data const seedAdmin = async () => { const hashedPassword = await User.hashPassword('Admin123!'); const admin = new User({ id: nextId++, email: 'admin@system.com', password: hashedPassword, name: 'System Administrator', role: 'admin', permissions: ['*'], // Super admin, all permissions isEmailVerified: true }); users.push(admin); const hashedUserPassword = await User.hashPassword('User123!'); const user = new User({ id: nextId++, email: 'user@example.com', password: hashedUserPassword, name: 'Regular User', role: 'user', permissions: ['read:profile', 'update:profile'], isEmailVerified: true }); users.push(user); const hashedModPassword = await User.hashPassword('Mod123!'); const moderator = new User({ id: nextId++, email: 'mod@system.com', password: hashedModPassword, name: 'Content Moderator', role: 'moderator', permissions: [ 'read:posts', 'create:posts', 'update:posts', 'delete:posts', 'read:users', 'update:users' ], isEmailVerified: true }); users.push(moderator); }; seedAdmin(); module.exports = { User, users, nextId: () => nextId++ };
C. Service Layer untuk Authentication
src/services/auth.service.js
javascript
/** * AUTHENTICATION SERVICE * Business logic untuk semua operasi autentikasi */ const jwt = require('jsonwebtoken'); const crypto = require('crypto'); const { User, users, nextId } = require('../models/User.model'); const config = require('../config/env'); const logger = require('../utils/logger'); class AuthService { constructor() { this.jwtSecret = config.JWT.SECRET; this.refreshSecret = config.JWT.REFRESH_SECRET; this.accessExpire = config.JWT.ACCESS_EXPIRE; this.refreshExpire = config.JWT.REFRESH_EXPIRE; } /** * ============= AUTHENTICATION ============= * Memverifikasi identitas user */ /** * Register user baru */ async register(userData) { try { // Validasi input this.validateRegistrationData(userData); // Cek email sudah terdaftar const existingUser = users.find(u => u.email === userData.email); if (existingUser) { throw { status: 409, code: 'EMAIL_EXISTS', message: 'Email sudah terdaftar' }; } // Hash password const hashedPassword = await User.hashPassword(userData.password); // Buat user baru const newUser = new User({ id: nextId(), email: userData.email.toLowerCase(), password: hashedPassword, name: userData.name, role: userData.role || 'user', permissions: this.getDefaultPermissions(userData.role || 'user'), isEmailVerified: false // Harus verifikasi email }); // Generate email verification token const verificationToken = newUser.generateEmailVerificationToken(); users.push(newUser); // Log activity logger.info(`User registered: ${newUser.email}`, { userId: newUser.id, role: newUser.role }); // Generate access token const accessToken = this.generateAccessToken(newUser); const refreshToken = this.generateRefreshToken(newUser); // Simpan refresh token newUser.addRefreshToken(refreshToken); // Kirim email verifikasi (simulasi) await this.sendVerificationEmail(newUser.email, verificationToken); return { user: newUser.toJSON(), tokens: { accessToken, refreshToken, expiresIn: this.getExpiresIn(this.accessExpire) }, requiresEmailVerification: true }; } catch (error) { logger.error('Registration error:', error); throw error; } } /** * Login user */ async login(email, password, deviceInfo = {}) { try { // Validasi input if (!email || !password) { throw { status: 400, code: 'MISSING_CREDENTIALS', message: 'Email dan password wajib diisi' }; } // Cari user const user = users.find(u => u.email === email.toLowerCase()); if (!user) { throw { status: 401, code: 'INVALID_CREDENTIALS', message: 'Email atau password salah' }; } // Cek account lock if (user.isLocked()) { const lockTimeRemaining = Math.ceil((user.lockUntil - Date.now()) / 60000); throw { status: 423, code: 'ACCOUNT_LOCKED', message: `Akun terkunci. Coba lagi dalam ${lockTimeRemaining} menit`, lockTimeRemaining }; } // Validasi password const isValidPassword = await user.validatePassword(password); if (!isValidPassword) { user.incrementLoginAttempts(); // Log failed attempt logger.warn(`Failed login attempt for: ${email}`, { attempts: user.loginAttempts, ip: deviceInfo.ip }); throw { status: 401, code: 'INVALID_CREDENTIALS', message: 'Email atau password salah', remainingAttempts: 5 - user.loginAttempts }; } // Cek account aktif if (!user.isActive) { throw { status: 403, code: 'ACCOUNT_INACTIVE', message: 'Akun tidak aktif. Hubungi administrator' }; } // Cek email verification if (!user.isEmailVerified) { throw { status: 403, code: 'EMAIL_NOT_VERIFIED', message: 'Email belum diverifikasi', resendVerification: true }; } // Reset login attempts on success user.resetLoginAttempts(); // Generate tokens const accessToken = this.generateAccessToken(user); const refreshToken = this.generateRefreshToken(user); // Simpan refresh token user.addRefreshToken(refreshToken); // Log successful login logger.info(`User logged in: ${user.email}`, { userId: user.id, deviceInfo }); return { user: user.toJSON(), tokens: { accessToken, refreshToken, expiresIn: this.getExpiresIn(this.accessExpire) } }; } catch (error) { logger.error('Login error:', error); throw error; } } /** * Refresh token */ async refreshToken(refreshToken) { try { if (!refreshToken) { throw { status: 401, code: 'NO_REFRESH_TOKEN', message: 'Refresh token required' }; } // Verify refresh token const decoded = jwt.verify(refreshToken, this.refreshSecret); // Find user const user = users.find(u => u.id === decoded.sub); if (!user) { throw { status: 401, code: 'USER_NOT_FOUND', message: 'User tidak ditemukan' }; } // Check if refresh token is valid if (!user.isValidRefreshToken(refreshToken)) { throw { status: 401, code: 'INVALID_REFRESH_TOKEN', message: 'Refresh token tidak valid' }; } // Generate new tokens const newAccessToken = this.generateAccessToken(user); const newRefreshToken = this.generateRefreshToken(user); // Revoke old refresh token and add new one user.revokeRefreshToken(refreshToken); user.addRefreshToken(newRefreshToken); return { tokens: { accessToken: newAccessToken, refreshToken: newRefreshToken, expiresIn: this.getExpiresIn(this.accessExpire) } }; } catch (error) { if (error.name === 'TokenExpiredError') { throw { status: 401, code: 'REFRESH_TOKEN_EXPIRED', message: 'Refresh token expired. Silakan login ulang' }; } if (error.name === 'JsonWebTokenError') { throw { status: 401, code: 'INVALID_REFRESH_TOKEN', message: 'Refresh token tidak valid' }; } throw error; } } /** * Logout user */ async logout(userId, refreshToken) { try { const user = users.find(u => u.id === userId); if (user && refreshToken) { user.revokeRefreshToken(refreshToken); logger.info(`User logged out: ${user.email}`, { userId: user.id }); } return { success: true }; } catch (error) { logger.error('Logout error:', error); throw error; } } /** * ============= AUTHORIZATION ============= * Memverifikasi hak akses user */ /** * Verify JWT token */ verifyToken(token, type = 'access') { try { const secret = type === 'access' ? this.jwtSecret : this.refreshSecret; const decoded = jwt.verify(token, secret, { issuer: config.JWT.ISSUER, audience: config.JWT.AUDIENCE }); return decoded; } catch (error) { if (error.name === 'TokenExpiredError') { throw { status: 401, code: 'TOKEN_EXPIRED', message: 'Token expired' }; } throw { status: 401, code: 'INVALID_TOKEN', message: 'Token tidak valid' }; } } /** * Check if user has role */ hasRole(user, allowedRoles) { if (!user || !user.role) return false; // Admin has all roles if (user.role === 'admin') return true; return allowedRoles.includes(user.role); } /** * Check if user has permission */ hasPermission(user, requiredPermissions) { if (!user || !user.permissions) return false; // Admin has all permissions if (user.role === 'admin' || user.permissions.includes('*')) { return true; } // Check each required permission return requiredPermissions.every(permission => user.permissions.includes(permission) ); } /** * Check if user owns resource */ isResourceOwner(user, resourceUserId) { return user.id === resourceUserId || user.role === 'admin'; } /** * Get user permissions */ getUserPermissions(user) { if (user.role === 'admin') { return ['*']; // Super admin } return user.permissions || []; } /** * ============= HELPER METHODS ============= */ /** * Generate access token */ generateAccessToken(user) { const payload = { sub: user.id, email: user.email, role: user.role, permissions: this.getUserPermissions(user), iss: config.JWT.ISSUER, aud: config.JWT.AUDIENCE, iat: Math.floor(Date.now() / 1000) }; return jwt.sign(payload, this.jwtSecret, { expiresIn: this.accessExpire }); } /** * Generate refresh token */ generateRefreshToken(user) { const payload = { sub: user.id, type: 'refresh', iss: config.JWT.ISSUER, aud: config.JWT.AUDIENCE, iat: Math.floor(Date.now() / 1000) }; return jwt.sign(payload, this.refreshSecret, { expiresIn: this.refreshExpire }); } /** * Get default permissions based on role */ getDefaultPermissions(role) { const permissions = { admin: ['*'], moderator: [ 'read:posts', 'create:posts', 'update:posts', 'delete:posts', 'read:users', 'update:users' ], user: [ 'read:profile', 'update:profile', 'read:posts', 'create:posts', 'update:own_posts', 'delete:own_posts' ] }; return permissions[role] || []; } /** * Validate registration data */ validateRegistrationData(data) { const errors = []; if (!data.email) { errors.push('Email wajib diisi'); } else if (!this.isValidEmail(data.email)) { errors.push('Format email tidak valid'); } if (!data.password) { errors.push('Password wajib diisi'); } else { const passwordErrors = this.validatePasswordStrength(data.password); errors.push(...passwordErrors); } if (!data.name) { errors.push('Nama wajib diisi'); } else if (data.name.length < 2) { errors.push('Nama minimal 2 karakter'); } if (errors.length > 0) { throw { status: 400, code: 'VALIDATION_ERROR', message: 'Validasi registrasi gagal', errors }; } } /** * Validate password strength */ validatePasswordStrength(password) { const errors = []; if (password.length < config.PASSWORD.MIN_LENGTH) { errors.push(`Password minimal ${config.PASSWORD.MIN_LENGTH} karakter`); } if (config.PASSWORD.REQUIRE_UPPERCASE && !/[A-Z]/.test(password)) { errors.push('Password harus mengandung huruf besar'); } if (config.PASSWORD.REQUIRE_LOWERCASE && !/[a-z]/.test(password)) { errors.push('Password harus mengandung huruf kecil'); } if (config.PASSWORD.REQUIRE_NUMBERS && !/\d/.test(password)) { errors.push('Password harus mengandung angka'); } if (config.PASSWORD.REQUIRE_SPECIAL && !/[!@#$%^&*(),.?":{}|<>]/.test(password)) { errors.push('Password harus mengandung karakter khusus'); } return errors; } /** * Validate email format */ isValidEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } /** * Get expires in seconds */ getExpiresIn(expireString) { const unit = expireString.slice(-1); const value = parseInt(expireString.slice(0, -1)); if (unit === 'm') return value * 60; if (unit === 'h') return value * 3600; if (unit === 'd') return value * 86400; return 900; // Default 15 minutes } /** * Send verification email (simulasi) */ async sendVerificationEmail(email, token) { // Dalam produksi: kirim email real console.log(` ========== EMAIL VERIFICATION ========== To: ${email} Subject: Verify Your Email Click the link below to verify your email: http://localhost:3000/api/auth/verify-email?token=${token} This link will expire in 24 hours. ========================================= `); return true; } /** * Send password reset email (simulasi) */ async sendPasswordResetEmail(email, token) { console.log(` ========== PASSWORD RESET ========== To: ${email} Subject: Reset Your Password Click the link below to reset your password: http://localhost:3000/api/auth/reset-password?token=${token} This link will expire in 1 hour. ===================================== `); return true; } } module.exports = new AuthService();
D. Permission System (RBAC + PBAC)
src/services/permission.service.js
javascript
/** * PERMISSION SERVICE * Role-Based Access Control (RBAC) + Permission-Based Access Control (PBAC) */ class PermissionService { constructor() { // Define permissions matrix this.permissionMatrix = { // Admin has all permissions admin: ['*'], // Moderator permissions moderator: [ 'read:posts', 'create:posts', 'update:posts', 'delete:posts', 'read:users', 'update:users', 'moderate:comments', 'read:analytics' ], // Regular user permissions user: [ 'read:profile', 'update:profile', 'read:posts', 'create:posts', 'update:own_posts', 'delete:own_posts', 'create:comments', 'update:own_comments', 'delete:own_comments' ], // Guest permissions guest: [ 'read:posts', 'read:public_profile' ] }; // Define resource ownership rules this.ownershipRules = { 'posts': (user, resource) => user.id === resource.authorId, 'comments': (user, resource) => user.id === resource.userId, 'profile': (user, resource) => user.id === resource.userId, 'orders': (user, resource) => user.id === resource.userId }; } /** * Check if user has role */ hasRole(user, requiredRoles) { if (!user || !user.role) return false; // Admin has all roles if (user.role === 'admin') return true; return requiredRoles.includes(user.role); } /** * Check if user has permission */ hasPermission(user, requiredPermissions, options = {}) { if (!user || !user.permissions) return false; const permissions = user.permissions; // Admin has all permissions if (user.role === 'admin' || permissions.includes('*')) { return true; } // Check each required permission return requiredPermissions.every(permission => { // Check direct permission if (permissions.includes(permission)) { return true; } // Check wildcard permissions (e.g., 'read:*' matches 'read:posts') const [action, resource] = permission.split(':'); return permissions.some(p => { const [pAction, pResource] = p.split(':'); if (pAction === action && pResource === '*') { return true; } if (pAction === '*' && pResource === resource) { return true; } return false; }); }); } /** * Check if user owns resource */ isOwner(user, resourceType, resource) { const rule = this.ownershipRules[resourceType]; if (!rule) { return false; } return rule(user, resource); } /** * Check if user can access resource */ canAccess(user, permission, resourceType = null, resource = null) { // First check permission if (!this.hasPermission(user, [permission])) { return false; } // If resource specified, check ownership if (resourceType && resource) { // Admin can access any resource if (user.role === 'admin') { return true; } // Check if permission is for own resources if (permission.includes('own_')) { return this.isOwner(user, resourceType, resource); } } return true; } /** * Get user's effective permissions */ getEffectivePermissions(user) { if (user.role === 'admin') { return ['*']; } const rolePermissions = this.permissionMatrix[user.role] || []; const customPermissions = user.permissions || []; // Merge and deduplicate return [...new Set([...rolePermissions, ...customPermissions])]; } /** * Filter resources based on permissions */ filterAccessibleResources(user, resources, permission, resourceType) { if (!resources || !Array.isArray(resources)) { return []; } return resources.filter(resource => { // Check permission if (!this.hasPermission(user, [permission])) { return false; } // Admin sees all if (user.role === 'admin') { return true; } // For 'own' permissions, check ownership if (permission.includes('own_')) { return this.isOwner(user, resourceType, resource); } return true; }); } /** * Get role hierarchy */ getRoleHierarchy() { return { 'admin': 100, 'moderator': 50, 'user': 10, 'guest': 0 }; } /** * Check if role has higher or equal priority */ hasHigherOrEqualRole(userRole, targetRole) { const hierarchy = this.getRoleHierarchy(); return hierarchy[userRole] >= hierarchy[targetRole]; } /** * Define custom permission rule */ defineRule(resourceType, ruleFunction) { this.ownershipRules[resourceType] = ruleFunction; } /** * Define role permissions */ defineRole(role, permissions) { this.permissionMatrix[role] = permissions; } } module.exports = new PermissionService();
E. Authentication Middleware
src/middleware/auth.middleware.js
javascript
/** * AUTHENTICATION & AUTHORIZATION MIDDLEWARE * Menangani verifikasi token dan hak akses */ const AuthService = require('../services/auth.service'); const PermissionService = require('../services/permission.service'); const { users } = require('../models/User.model'); const logger = require('../utils/logger'); class AuthMiddleware { /** * ============= AUTHENTICATION ============= */ /** * Authenticate JWT token * Middleware untuk memverifikasi token dan mendapatkan user */ authenticate() { return async (req, res, next) => { try { // Extract token dari header const token = this.extractToken(req); if (!token) { return res.status(401).json({ success: false, error: 'NO_TOKEN', message: 'Authentication required', code: 'AUTH_REQUIRED' }); } // Verify token const decoded = AuthService.verifyToken(token, 'access'); // Get user from database const user = users.find(u => u.id === decoded.sub); if (!user) { return res.status(401).json({ success: false, error: 'USER_NOT_FOUND', message: 'User tidak ditemukan' }); } // Check if user is active if (!user.isActive) { return res.status(403).json({ success: false, error: 'ACCOUNT_INACTIVE', message: 'Akun tidak aktif' }); } // Attach user and token to request req.user = user; req.token = token; req.decodedToken = decoded; // Log authenticated request logger.debug('User authenticated', { userId: user.id, role: user.role, path: req.path }); next(); } catch (error) { // Handle specific JWT errors if (error.code === 'TOKEN_EXPIRED') { return res.status(401).json({ success: false, error: 'TOKEN_EXPIRED', message: 'Token expired. Silakan refresh token', canRefresh: true }); } if (error.code === 'INVALID_TOKEN') { return res.status(401).json({ success: false, error: 'INVALID_TOKEN', message: 'Token tidak valid' }); } // Generic error return res.status(401).json({ success: false, error: 'AUTH_FAILED', message: 'Authentication failed' }); } }; } /** * Optional authentication * Tidak error jika token tidak ada, user = null */ optional() { return async (req, res, next) => { try { const token = this.extractToken(req); if (token) { const decoded = AuthService.verifyToken(token, 'access'); const user = users.find(u => u.id === decoded.sub); if (user && user.isActive) { req.user = user; req.token = token; } } next(); } catch { // Silent fail, proceed without user next(); } }; } /** * Verify email token */ verifyEmailToken() { return async (req, res, next) => { try { const { token } = req.query; if (!token) { return res.status(400).json({ success: false, error: 'NO_TOKEN', message: 'Verification token required' }); } // Find user by token const hashedToken = require('crypto') .createHash('sha256') .update(token) .digest('hex'); const user = users.find(u => u.emailVerificationToken === hashedToken); if (!user) { return res.status(400).json({ success: false, error: 'INVALID_TOKEN', message: 'Token verifikasi tidak valid atau expired' }); } req.userToVerify = user; req.verificationToken = token; next(); } catch (error) { next(error); } }; } /** * ============= AUTHORIZATION ============= */ /** * Authorize by role * Middleware untuk membatasi akses berdasarkan role */ authorize(...allowedRoles) { return (req, res, next) => { try { if (!req.user) { return res.status(401).json({ success: false, error: 'NOT_AUTHENTICATED', message: 'Authentication required' }); } // Check if user has required role if (!AuthService.hasRole(req.user, allowedRoles)) { logger.warn('Authorization failed - insufficient role', { userId: req.user.id, userRole: req.user.role, requiredRoles: allowedRoles, path: req.path }); return res.status(403).json({ success: false, error: 'FORBIDDEN', message: 'Anda tidak memiliki akses ke resource ini', requiredRoles: allowedRoles, yourRole: req.user.role }); } next(); } catch (error) { next(error); } }; } /** * Authorize by permission * Middleware untuk membatasi akses berdasarkan permission */ hasPermission(...requiredPermissions) { return (req, res, next) => { try { if (!req.user) { return res.status(401).json({ success: false, error: 'NOT_AUTHENTICATED', message: 'Authentication required' }); } // Check if user has required permissions if (!PermissionService.hasPermission(req.user, requiredPermissions)) { logger.warn('Authorization failed - insufficient permissions', { userId: req.user.id, userRole: req.user.role, requiredPermissions, userPermissions: req.user.permissions, path: req.path }); return res.status(403).json({ success: false, error: 'FORBIDDEN', message: 'Anda tidak memiliki izin untuk melakukan aksi ini', requiredPermissions, yourPermissions: req.user.permissions || [] }); } next(); } catch (error) { next(error); } }; } /** * Authorize resource owner * Middleware untuk cek kepemilikan resource */ canAccess(resourceType, resourceIdParam = 'id', ownerField = 'userId') { return async (req, res, next) => { try { if (!req.user) { return res.status(401).json({ success: false, error: 'NOT_AUTHENTICATED', message: 'Authentication required' }); } const resourceId = req.params[resourceIdParam]; // Get resource from database (simulasi) const resource = await this.getResource(resourceType, resourceId); if (!resource) { return res.status(404).json({ success: false, error: 'NOT_FOUND', message: `${resourceType} tidak ditemukan` }); } // Check ownership const isOwner = PermissionService.isOwner( req.user, resourceType, resource ); // Admin can access any resource if (req.user.role === 'admin' || isOwner) { req.resource = resource; return next(); } // Check if user has permission to access any resource of this type if (PermissionService.hasPermission(req.user, [`read:${resourceType}`])) { req.resource = resource; return next(); } logger.warn('Authorization failed - not owner', { userId: req.user.id, resourceType, resourceId, path: req.path }); return res.status(403).json({ success: false, error: 'FORBIDDEN', message: 'Anda tidak memiliki akses ke resource ini' }); } catch (error) { next(error); } }; } /** * Rate limit untuk authentication attempts */ authRateLimiter() { const attempts = new Map(); return (req, res, next) => { const identifier = req.body.email || req.ip; const now = Date.now(); const windowMs = 15 * 60 * 1000; // 15 minutes const maxAttempts = 5; const userAttempts = attempts.get(identifier) || []; const recentAttempts = userAttempts.filter( time => now - time < windowMs ); if (recentAttempts.length >= maxAttempts) { const oldestAttempt = recentAttempts[0]; const resetTime = Math.ceil( (oldestAttempt + windowMs - now) / 1000 ); return res.status(429).json({ success: false, error: 'RATE_LIMIT_EXCEEDED', message: 'Terlalu banyak percobaan login. Coba lagi nanti', retryAfter: resetTime }); } recentAttempts.push(now); attempts.set(identifier, recentAttempts); next(); }; } /** * ============= HELPER METHODS ============= */ /** * Extract token from request */ extractToken(req) { // Check Authorization header const authHeader = req.headers.authorization; if (authHeader && authHeader.startsWith('Bearer ')) { return authHeader.split(' ')[1]; } // Check query parameter if (req.query.token) { return req.query.token; } // Check cookie if (req.cookies && req.cookies.token) { return req.cookies.token; } return null; } /** * Get resource from database (simulasi) */ async getResource(resourceType, id) { const idNum = parseInt(id); switch (resourceType) { case 'post': // Simulasi post return { id: 1, title: 'Test Post', authorId: 1, userId: 1 }; case 'comment': return { id: 1, content: 'Test Comment', userId: 1 }; case 'user': return { id: idNum, userId: idNum }; default: return null; } } } module.exports = new AuthMiddleware();
F. Controllers untuk Authentication
src/controllers/auth.controller.js
javascript
/** * AUTH CONTROLLER * Handle HTTP requests untuk authentication & authorization */ const AuthService = require('../services/auth.service'); const { users } = require('../models/User.model'); const logger = require('../utils/logger'); class AuthController { /** * POST /api/auth/register * Register user baru */ async register(req, res, next) { try { const result = await AuthService.register(req.body); res.status(201).json({ success: true, message: 'Registrasi berhasil! Silakan cek email untuk verifikasi.', data: result }); } catch (error) { next(error); } } /** * POST /api/auth/login * Login user */ async login(req, res, next) { try { const { email, password } = req.body; // Get device info for logging const deviceInfo = { ip: req.ip || req.connection.remoteAddress, userAgent: req.headers['user-agent'], platform: req.headers['sec-ch-ua-platform'] }; const result = await AuthService.login(email, password, deviceInfo); // Set refresh token in HTTP-only cookie res.cookie('refreshToken', result.tokens.refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days }); res.status(200).json({ success: true, message: 'Login berhasil', data: { user: result.user, tokens: { accessToken: result.tokens.accessToken, expiresIn: result.tokens.expiresIn } } }); } catch (error) { next(error); } } /** * POST /api/auth/refresh * Refresh access token */ async refreshToken(req, res, next) { try { const refreshToken = req.body.refreshToken || req.cookies.refreshToken; const result = await AuthService.refreshToken(refreshToken); // Update refresh token cookie res.cookie('refreshToken', result.tokens.refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 }); res.status(200).json({ success: true, message: 'Token refreshed', data: { accessToken: result.tokens.accessToken, expiresIn: result.tokens.expiresIn } }); } catch (error) { next(error); } } /** * POST /api/auth/logout * Logout user */ async logout(req, res, next) { try { const refreshToken = req.body.refreshToken || req.cookies.refreshToken; await AuthService.logout(req.user?.id, refreshToken); // Clear refresh token cookie res.clearCookie('refreshToken'); res.status(200).json({ success: true, message: 'Logout berhasil' }); } catch (error) { next(error); } } /** * GET /api/auth/me * Get current user profile */ async getProfile(req, res, next) { try { res.status(200).json({ success: true, data: { user: req.user.toJSON(), permissions: req.user.permissions || [] } }); } catch (error) { next(error); } } /** * GET /api/auth/verify-email * Verify email address */ async verifyEmail(req, res, next) { try { const { token } = req.query; // Find user by token const hashedToken = require('crypto') .createHash('sha256') .update(token) .digest('hex'); const user = users.find(u => u.emailVerificationToken === hashedToken); if (!user) { return res.status(400).json({ success: false, error: 'INVALID_TOKEN', message: 'Token verifikasi tidak valid atau sudah kadaluarsa' }); } // Verify email user.isEmailVerified = true; user.emailVerificationToken = null; logger.info(`Email verified: ${user.email}`, { userId: user.id }); res.status(200).json({ success: true, message: 'Email berhasil diverifikasi! Silakan login.' }); } catch (error) { next(error); } } /** * POST /api/auth/forgot-password * Request password reset */ async forgotPassword(req, res, next) { try { const { email } = req.body; const user = users.find(u => u.email === email.toLowerCase()); if (user) { const resetToken = user.generatePasswordResetToken(); await AuthService.sendPasswordResetEmail(email, resetToken); } // Always return success to prevent email enumeration res.status(200).json({ success: true, message: 'Jika email terdaftar, link reset password akan dikirim' }); } catch (error) { next(error); } } /** * POST /api/auth/reset-password * Reset password with token */ async resetPassword(req, res, next) { try { const { token, newPassword } = req.body; // Validate password strength const passwordErrors = AuthService.validatePasswordStrength(newPassword); if (passwordErrors.length > 0) { return res.status(400).json({ success: false, error: 'VALIDATION_ERROR', message: 'Password tidak memenuhi kriteria', errors: passwordErrors }); } // Find user by reset token const hashedToken = require('crypto') .createHash('sha256') .update(token) .digest('hex'); const user = users.find(u => u.passwordResetToken === hashedToken && u.passwordResetExpires > Date.now() ); if (!user) { return res.status(400).json({ success: false, error: 'INVALID_TOKEN', message: 'Token reset password tidak valid atau sudah kadaluarsa' }); } // Update password user.password = await User.hashPassword(newPassword); user.passwordResetToken = null; user.passwordResetExpires = null; // Revoke all refresh tokens for security user.refreshTokens = []; logger.info(`Password reset successful: ${user.email}`, { userId: user.id }); res.status(200).json({ success: true, message: 'Password berhasil direset! Silakan login dengan password baru.' }); } catch (error) { next(error); } } /** * GET /api/auth/permissions * Get user permissions */ async getPermissions(req, res, next) { try { const permissions = req.user.permissions || []; res.status(200).json({ success: true, data: { role: req.user.role, permissions, isAdmin: req.user.role === 'admin', can: (permission) => permissions.includes(permission) } }); } catch (error) { next(error); } } } module.exports = new AuthController();
G. Routes untuk Authentication
src/routes/auth.routes.js
javascript
/** * AUTH ROUTES * Definisi endpoint untuk authentication & authorization */ const express = require('express'); const router = express.Router(); const AuthController = require('../controllers/auth.controller'); const AuthMiddleware = require('../middleware/auth.middleware'); const { validate } = require('../middleware/validation.middleware'); /** * @swagger * tags: * name: Authentication * description: User authentication and authorization */ // ============= PUBLIC ROUTES ============= /** * @swagger * /auth/register: * post: * summary: Register new user * tags: [Authentication] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - email * - password * - name * properties: * email: * type: string * format: email * password: * type: string * format: password * name: * type: string * role: * type: string * enum: [user, moderator] * responses: * 201: * description: Registration successful * 400: * description: Validation error * 409: * description: Email already exists */ router.post('/register', AuthMiddleware.authRateLimiter(), validate('register'), AuthController.register ); /** * @swagger * /auth/login: * post: * summary: Login user * tags: [Authentication] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - email * - password * properties: * email: * type: string * format: email * password: * type: string * format: password * responses: * 200: * description: Login successful * 401: * description: Invalid credentials * 403: * description: Email not verified / Account inactive * 423: * description: Account locked */ router.post('/login', AuthMiddleware.authRateLimiter(), validate('login'), AuthController.login ); /** * @swagger * /auth/refresh: * post: * summary: Refresh access token * tags: [Authentication] * requestBody: * content: * application/json: * schema: * type: object * properties: * refreshToken: * type: string * responses: * 200: * description: Token refreshed * 401: * description: Invalid/expired refresh token */ router.post('/refresh', AuthController.refreshToken ); /** * @swagger * /auth/verify-email: * get: * summary: Verify email address * tags: [Authentication] * parameters: * - in: query * name: token * schema: * type: string * required: true * description: Email verification token * responses: * 200: * description: Email verified successfully * 400: * description: Invalid/expired token */ router.get('/verify-email', AuthController.verifyEmail ); /** * @swagger * /auth/forgot-password: * post: * summary: Request password reset * tags: [Authentication] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - email * properties: * email: * type: string * format: email * responses: * 200: * description: Reset link sent (if email exists) */ router.post('/forgot-password', validate('forgotPassword'), AuthController.forgotPassword ); /** * @swagger * /auth/reset-password: * post: * summary: Reset password with token * tags: [Authentication] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - token * - newPassword * properties: * token: * type: string * newPassword: * type: string * format: password * responses: * 200: * description: Password reset successful * 400: * description: Invalid/expired token or weak password */ router.post('/reset-password', validate('resetPassword'), AuthController.resetPassword ); // ============= PROTECTED ROUTES ============= /** * @swagger * /auth/me: * get: * summary: Get current user profile * tags: [Authentication] * security: * - bearerAuth: [] * responses: * 200: * description: User profile retrieved * 401: * description: Unauthorized */ router.get('/me', AuthMiddleware.authenticate(), AuthController.getProfile ); /** * @swagger * /auth/logout: * post: * summary: Logout user * tags: [Authentication] * security: * - bearerAuth: [] * responses: * 200: * description: Logout successful */ router.post('/logout', AuthMiddleware.authenticate(), AuthController.logout ); /** * @swagger * /auth/permissions: * get: * summary: Get user permissions * tags: [Authentication] * security: * - bearerAuth: [] * responses: * 200: * description: User permissions retrieved */ router.get('/permissions', AuthMiddleware.authenticate(), AuthController.getPermissions ); // ============= ADMIN ROUTES ============= /** * @swagger * /auth/admin/users: * get: * summary: Get all users (admin only) * tags: [Authentication] * security: * - bearerAuth: [] * responses: * 200: * description: Users retrieved * 403: * description: Forbidden (requires admin role) */ router.get('/admin/users', AuthMiddleware.authenticate(), AuthMiddleware.authorize('admin'), (req, res) => { const { users } = require('../models/User.model'); res.json({ success: true, data: users.map(u => u.toJSON()) }); } ); module.exports = router;
3.5 JWT Token Structure & Flow
javascript
/** * JWT (JSON Web Token) STRUCTURE * * 1. HEADER (Algorithm & Token Type) * { * "alg": "HS256", * "typ": "JWT" * } * * 2. PAYLOAD (Data) * { * "sub": 1, // Subject (user ID) * "email": "user@example.com", // User email * "role": "admin", // User role * "permissions": [...], // User permissions * "iss": "auth-system", // Issuer * "aud": "auth-client", // Audience * "iat": 1709462400, // Issued at * "exp": 1709463300, // Expiration * "jti": "uuid-v4" // JWT ID (optional) * } * * 3. SIGNATURE * HMACSHA256( * base64UrlEncode(header) + "." + * base64UrlEncode(payload), * secret * ) * * FINAL JWT: * eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. * eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. * SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c */ /** * JWT AUTHENTICATION FLOW * * ┌─────────┐ ┌─────────┐ ┌─────────┐ * │ CLIENT │ │ SERVER │ │ DATABASE│ * └────┬────┘ └────┬────┘ └────┬────┘ * │ │ │ * │ POST /login │ │ * │──────────────────>│ │ * │ {email,pass} │ │ * │ │ │ * │ │ Verify User │ * │ │──────────────────>│ * │ │ │ * │ │ User Found │ * │ │<──────────────────│ * │ │ │ * │ │ Generate JWT │ * │ │ (Sign) │ * │ │ │ * │ 200 OK + JWT │ │ * │<──────────────────│ │ * │ │ │ * │ │ │ * │ GET /profile │ │ * │──────────────────>│ │ * │ Auth: Bearer JWT│ │ * │ │ │ * │ │ Verify JWT │ * │ │ (Decode + Check)│ * │ │ │ * │ │ Get User by ID │ * │ │──────────────────>│ * │ │ │ * │ │ User Data │ * │ │<──────────────────│ * │ │ │ * │ 200 OK + Data │ │ * │<──────────────────│ │ * ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ * │ CLIENT │ │ SERVER │ │ DATABASE│ * └─────────┘ └─────────┘ └─────────┘ */
3.6 Session vs JWT vs OAuth
javascript
/** * COMPARISON: AUTHENTICATION METHODS */ const authComparison = { /** * 1. SESSION-BASED AUTHENTICATION * Pros: Simple, server control, easy revocation * Cons: Stateful, not scalable horizontally */ session: { how: "Server menyimpan session di memory/database", flow: ` 1. Client login → Server create session 2. Server return session ID in cookie 3. Client sends cookie with every request 4. Server validates session from store `, pros: [ "Instant revocation (delete session)", "No token exposure in JavaScript", "Works with legacy systems" ], cons: [ "Stateful - server must store sessions", "Hard to scale horizontally", "Session fixation attacks", "Memory intensive" ], bestFor: [ "Traditional web apps", "Small to medium applications", "Internal tools", "Systems requiring instant logout" ] }, /** * 2. JWT (JSON Web Token) BASED * Pros: Stateless, scalable, cross-domain * Cons: Cannot revoke, token size, storage concerns */ jwt: { how: "Server menandatangani token, client menyimpan", flow: ` 1. Client login → Server generate JWT 2. Server return JWT to client 3. Client stores JWT (localStorage/memory) 4. Client sends JWT in Authorization header 5. Server verifies signature, no DB lookup `, pros: [ "Stateless - no session storage", "Horizontally scalable", "Cross-domain / CORS friendly", "Can include custom data", "Mobile app friendly" ], cons: [ "Cannot revoke (until expiry)", "Token size grows with claims", "Storage concerns (XSS)", "No built-in refresh mechanism" ], bestFor: [ "REST APIs", "Microservices", "Mobile applications", "SPA (Single Page Apps)", "Stateless architectures" ] }, /** * 3. OAUTH 2.0 / OPENID CONNECT * Pros: Delegated auth, 3rd party login, industry standard * Cons: Complex, requires redirects, multiple flows */ oauth: { how: "Delegated authorization via third-party", flow: ` 1. Client requests authorization 2. Redirect to auth provider 3. User logs in with provider 4. Provider returns authorization code 5. Exchange code for access token 6. Access resources with token `, pros: [ "No password storage needed", "Users trust familiar providers", "Rich permission scoping", "Industry standard", "Social login (Google, Facebook)" ], cons: [ "Complex implementation", "Requires HTTPS redirects", "Third-party dependency", "Multiple flow types" ], bestFor: [ "Third-party authentication", "Social login features", "Enterprise SSO", "Mobile apps with social login" ] }, /** * 4. API KEY AUTHENTICATION * Pros: Simple, machine-to-machine * Cons: Less secure, no user context */ apiKey: { how: "Pre-shared key in headers", pros: [ "Very simple to implement", "No user interaction needed", "Good for service accounts" ], cons: [ "No user context", "Key rotation difficult", "Less secure (static keys)", "No expiration by default" ], bestFor: [ "Public APIs with rate limiting", "Service-to-service communication", "Developer APIs", "IoT devices" ] } }; /** * CHOOSING THE RIGHT AUTHENTICATION */ function chooseAuthMethod(req, res) { const factors = { needStateless: true, // Scale horizontally? needRevocation: false, // Need instant logout? isMobileApp: true, // Mobile or web? thirdPartyLogin: false, // Use Google/Facebook? machineToMachine: false // API for other services? }; if (factors.machineToMachine) { return 'API Key + JWT for service accounts'; } if (factors.thirdPartyLogin) { return 'OAuth 2.0 + JWT for your own sessions'; } if (factors.needStateless && !factors.needRevocation) { return 'JWT with short expiry + refresh tokens'; } if (factors.needRevocation && !factors.needStateless) { return 'Session-based authentication'; } // Hybrid approach return 'JWT for API, Session for web, OAuth for social'; }
3.7 Role & Permission Matrix
javascript
/** * ROLE-BASED ACCESS CONTROL (RBAC) MATRIX */ const rbacMatrix = { // Role definitions roles: { admin: { level: 100, description: 'Full system access', permissions: ['*'] }, moderator: { level: 50, description: 'Content management', permissions: [ 'read:posts', 'create:posts', 'update:posts', 'delete:posts', 'read:comments', 'delete:comments', 'read:users', 'update:users', 'read:analytics' ] }, user: { level: 10, description: 'Regular user', permissions: [ 'read:profile', 'update:profile', 'read:posts', 'create:posts', 'update:own_posts', 'delete:own_posts', 'create:comments', 'update:own_comments', 'delete:own_comments' ] }, guest: { level: 0, description: 'Unauthenticated', permissions: [ 'read:posts', 'read:public_profile' ] } }, // Permission categories categories: { profile: { read: 'View profile information', update: 'Update own profile', delete: 'Delete account' }, posts: { read: 'View posts', create: 'Create new posts', update: 'Update any post', delete: 'Delete any post', 'update:own': 'Update own posts', 'delete:own': 'Delete own posts' }, comments: { read: 'View comments', create: 'Create comments', update: 'Update any comment', delete: 'Delete any comment', 'update:own': 'Update own comments', 'delete:own': 'Delete own comments' }, users: { read: 'View users list', update: 'Update users', delete: 'Delete users' }, admin: { access: 'Access admin panel', config: 'Modify system config', logs: 'View system logs' } }, // Helper: Check if user has permission checkPermission(user, requiredPermission) { if (!user || !user.role) return false; const role = this.roles[user.role]; if (!role) return false; // Admin has all permissions if (role.permissions.includes('*')) return true; // Check exact permission if (role.permissions.includes(requiredPermission)) return true; // Check wildcard permissions (e.g., 'read:*') const [action, resource] = requiredPermission.split(':'); return role.permissions.some(p => { const [pAction, pResource] = p.split(':'); if (pAction === action && pResource === '*') return true; if (pAction === '*' && pResource === resource) return true; if (p === `${action}:${resource}`) return true; return false; }); }, // Helper: Get all permissions for role getPermissionsForRole(roleName) { const role = this.roles[roleName]; return role ? role.permissions : []; } }; /** * PERMISSION-BASED ACCESS CONTROL (PBAC) MATRIX * More granular than RBAC, allows custom permissions per user */ const pbacMatrix = { // Permission categories resources: { posts: ['create', 'read', 'update', 'delete'], comments: ['create', 'read', 'update', 'delete'], users: ['read', 'update', 'delete'], profile: ['read', 'update'], analytics: ['read'], settings: ['read', 'update'] }, // Permission strings format: "action:resource" // Examples: // - 'create:posts' → Create posts // - 'read:users' → Read users // - 'update:profile' → Update profile // - 'delete:comments' → Delete comments // - 'read:*' → Read all resources // - '*:posts' → All actions on posts // - '*' → All permissions // Helper: Validate permission format isValidPermission(permission) { if (permission === '*') return true; const parts = permission.split(':'); if (parts.length !== 2) return false; const [action, resource] = parts; if (action === '*') return true; if (resource === '*') return true; return this.resources[resource] && this.resources[resource].includes(action); }, // Helper: Generate permission from action and resource generatePermission(action, resource) { if (action === 'all' && resource === 'all') return '*'; if (action === 'all') return `*:${resource}`; if (resource === 'all') return `${action}:*`; return `${action}:${resource}`; } };
3.8 Security Best Practices untuk Authentication
javascript
/** * AUTHENTICATION SECURITY BEST PRACTICES */ const securityBestPractices = { /** * 1. PASSWORD STORAGE */ passwordStorage: { do: [ "Use bcrypt, scrypt, or argon2 for hashing", "Salt rounds: 10-12 (bcrypt)", "Minimum password length: 8 characters", "Enforce password complexity" ], dont: [ "Never store plain text passwords", "Don't use MD5, SHA1, or SHA256 without salt", "Don't limit password length excessively", "Don't truncate passwords" ], example: ` const bcrypt = require('bcryptjs'); const saltRounds = 10; // Hash password const hash = await bcrypt.hash(password, saltRounds); // Verify password const isValid = await bcrypt.compare(password, hash); ` }, /** * 2. JWT SECURITY */ jwtSecurity: { do: [ "Use strong secret keys (min 32 chars)", "Set short expiration times (15m-1h)", "Use refresh tokens for long-lived sessions", "Store JWT in HTTP-only cookies", "Include issuer and audience claims", "Use HTTPS in production" ], dont: [ "Don't store sensitive data in JWT payload", "Don't accept tokens without expiration", "Don't use weak signing algorithms (HS256 is fine)", "Don't store JWT in localStorage (XSS risk)", "Don't log JWT tokens" ], example: ` const jwt = require('jsonwebtoken'); // Generate token const token = jwt.sign( { sub: user.id, role: user.role, iss: 'myapp', aud: 'myapp-client' }, process.env.JWT_SECRET, { expiresIn: '15m' } ); // Verify token const decoded = jwt.verify(token, process.env.JWT_SECRET); ` }, /** * 3. RATE LIMITING */ rateLimiting: { do: [ "Limit login attempts (5 per 15 minutes)", "Limit registration (3 per hour per IP)", "Limit password reset requests", "Use exponential backoff", "Implement account lockout after failed attempts" ], dont: [ "Don't rely on client-side rate limiting", "Don't use the same limit for all endpoints", "Don't forget to count failed attempts" ], example: ` const rateLimit = require('express-rate-limit'); const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 attempts skipSuccessfulRequests: true, message: 'Too many login attempts' }); app.post('/login', loginLimiter, loginHandler); ` }, /** * 4. ACCOUNT RECOVERY */ accountRecovery: { do: [ "Send password reset via email only", "Use short-lived tokens (1 hour)", "Invalidate token after use", "Notify user of password changes", "Use secure token generation (crypto.randomBytes)" ], dont: [ "Don't email passwords", "Don't use sequential reset tokens", "Don't allow password reset without email verification", "Don't reveal if email exists" ], example: ` // Generate secure reset token const crypto = require('crypto'); const token = crypto.randomBytes(32).toString('hex'); const hashedToken = crypto .createHash('sha256') .update(token) .digest('hex'); // Store hashed token, send raw token via email ` }, /** * 5. CORS & COOKIES */ corsAndCookies: { do: [ "Set CORS properly (whitelist origins)", "Use HTTP-only cookies for refresh tokens", "Set Secure flag in production", "Set SameSite=Strict or SameSite=Lax", "Use CSRF tokens for state-changing operations" ], dont: [ "Don't use wildcard CORS with credentials", "Don't expose refresh tokens to JavaScript", "Don't set cookies without secure flags" ], example: ` // HTTP-only cookie res.cookie('refreshToken', token, { httpOnly: true, // Not accessible via JS secure: true, // HTTPS only sameSite: 'strict', // CSRF protection maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days }); ` }, /** * 6. AUTHORIZATION */ authorization: { do: [ "Check permissions on every request", "Use principle of least privilege", "Implement role-based access control", "Verify resource ownership", "Log authorization failures" ], dont: [ "Don't rely only on frontend authorization", "Don't assume user has permission without checking", "Don't use insecure direct object references" ], example: ` // Check ownership if (req.user.role !== 'admin' && resource.userId !== req.user.id) { return res.status(403).json({ error: 'FORBIDDEN', message: 'Not authorized' }); } ` } };
3.9 Testing Authentication & Authorization
tests/auth/authentication.test.js
javascript
/** * TESTING AUTHENTICATION & AUTHORIZATION */ const request = require('supertest'); const app = require('../../src/app'); const { users } = require('../../src/models/User.model'); describe('Authentication System', () => { beforeEach(() => { // Reset users or use test database }); describe('POST /api/auth/register', () => { it('should register new user successfully', async () => { const response = await request(app) .post('/api/auth/register') .send({ email: 'test@example.com', password: 'Test123!', name: 'Test User' }); expect(response.statusCode).toBe(201); expect(response.body.success).toBe(true); expect(response.body.data.user).toHaveProperty('id'); expect(response.body.data.user.email).toBe('test@example.com'); expect(response.body.data.tokens).toHaveProperty('accessToken'); }); it('should return 409 if email already exists', async () => { await request(app) .post('/api/auth/register') .send({ email: 'duplicate@example.com', password: 'Test123!', name: 'Test User' }); const response = await request(app) .post('/api/auth/register') .send({ email: 'duplicate@example.com', password: 'Test123!', name: 'Another User' }); expect(response.statusCode).toBe(409); expect(response.body.error).toBe('EMAIL_EXISTS'); }); it('should validate password strength', async () => { const response = await request(app) .post('/api/auth/register') .send({ email: 'weak@example.com', password: 'weak', name: 'Weak Password' }); expect(response.statusCode).toBe(400); expect(response.body.error).toBe('VALIDATION_ERROR'); expect(response.body.errors).toBeDefined(); }); }); describe('POST /api/auth/login', () => { it('should login successfully with valid credentials', async () => { const response = await request(app) .post('/api/auth/login') .send({ email: 'admin@system.com', password: 'Admin123!' }); expect(response.statusCode).toBe(200); expect(response.body.data).toHaveProperty('user'); expect(response.body.data).toHaveProperty('tokens'); expect(response.headers['set-cookie']).toBeDefined(); }); it('should return 401 with invalid credentials', async () => { const response = await request(app) .post('/api/auth/login') .send({ email: 'admin@system.com', password: 'wrongpassword' }); expect(response.statusCode).toBe(401); expect(response.body.error).toBe('INVALID_CREDENTIALS'); }); it('should lock account after multiple failed attempts', async () => { // 5 failed attempts for (let i = 0; i < 5; i++) { await request(app) .post('/api/auth/login') .send({ email: 'admin@system.com', password: 'wrongpassword' }); } const response = await request(app) .post('/api/auth/login') .send({ email: 'admin@system.com', password: 'Admin123!' }); expect(response.statusCode).toBe(423); expect(response.body.error).toBe('ACCOUNT_LOCKED'); }); }); describe('Protected Routes', () => { let authToken; beforeEach(async () => { const loginResponse = await request(app) .post('/api/auth/login') .send({ email: 'user@example.com', password: 'User123!' }); authToken = loginResponse.body.data.tokens.accessToken; }); it('should access protected route with valid token', async () => { const response = await request(app) .get('/api/auth/me') .set('Authorization', `Bearer ${authToken}`); expect(response.statusCode).toBe(200); expect(response.body.data.user).toBeDefined(); }); it('should return 401 without token', async () => { const response = await request(app) .get('/api/auth/me'); expect(response.statusCode).toBe(401); expect(response.body.error).toBe('NO_TOKEN'); }); it('should return 401 with invalid token', async () => { const response = await request(app) .get('/api/auth/me') .set('Authorization', 'Bearer invalid.token.here'); expect(response.statusCode).toBe(401); expect(response.body.error).toBe('INVALID_TOKEN'); }); it('should enforce role-based access control', async () => { // User trying to access admin route const response = await request(app) .get('/api/auth/admin/users') .set('Authorization', `Bearer ${authToken}`); expect(response.statusCode).toBe(403); expect(response.body.error).toBe('FORBIDDEN'); }); it('should allow admin access', async () => { const adminLogin = await request(app) .post('/api/auth/login') .send({ email: 'admin@system.com', password: 'Admin123!' }); const adminToken = adminLogin.body.data.tokens.accessToken; const response = await request(app) .get('/api/auth/admin/users') .set('Authorization', `Bearer ${adminToken}`); expect(response.statusCode).toBe(200); expect(response.body.data).toBeInstanceOf(Array); }); }); describe('Token Refresh', () => { it('should refresh access token with valid refresh token', async () => { const loginResponse = await request(app) .post('/api/auth/login') .send({ email: 'user@example.com', password: 'User123!' }); const refreshToken = loginResponse.body.data.tokens.refreshToken; const refreshResponse = await request(app) .post('/api/auth/refresh') .send({ refreshToken }); expect(refreshResponse.statusCode).toBe(200); expect(refreshResponse.body.data).toHaveProperty('accessToken'); expect(refreshResponse.body.data).toHaveProperty('expiresIn'); }); it('should return 401 with invalid refresh token', async () => { const response = await request(app) .post('/api/auth/refresh') .send({ refreshToken: 'invalid.token' }); expect(response.statusCode).toBe(401); expect(response.body.error).toBe('INVALID_REFRESH_TOKEN'); }); }); });
3.10 Latihan Praktikum
Exercise 1: Social Login Integration
javascript
/** * TODO: Implement OAuth 2.0 dengan Google * - Setup Google Cloud Console * - Implement OAuth2 flow * - Link social account ke user existing * - Generate JWT setelah OAuth success */ class GoogleOAuthService { async getAuthorizationUrl() { // Implementasi } async handleCallback(code) { // Implementasi } async findOrCreateUser(profile) { // Implementasi } }
Exercise 2: Two-Factor Authentication (2FA)
javascript
/** * TODO: Implement Two-Factor Authentication * - Generate QR code untuk Google Authenticator * - Verify TOTP code * - Backup codes * - Remember device option */ class TwoFactorService { async generateSecret(userId) { // Implementasi } async verifyToken(secret, token) { // Implementasi } async generateBackupCodes() { // Implementasi } }
Exercise 3: Permission Management Dashboard
javascript
/** * TODO: Build admin dashboard untuk manage permissions * - CRUD roles * - Assign permissions to roles * - Assign roles to users * - Custom permissions per user */ class PermissionManager { async createRole(roleData) { // Implementasi } async assignPermission(roleId, permission) { // Implementasi } async getUserPermissions(userId) { // Implementasi } }
Exercise 4: Audit Log untuk Authentication
javascript
/** * TODO: Implement audit logging untuk security events * - Log all login attempts (success/fail) * - Log password changes * - Log permission changes * - Log account locks/unlocks */ class AuthAuditLogger { async logLoginAttempt(email, success, ip, userAgent) { // Implementasi } async logPasswordChange(userId, changedBy) { // Implementasi } async getSecurityAudit(userId) { // Implementasi } }
4. Daftar Pustaka
- Medium (2024). Building an Authentication and Authorization API with Express.js. https://medium.com/@pirson
- Karisma Academy (2026). JWT Authentication di Express.js untuk Pemula. https://blog.karismaacademy.com
- ApiDog (2025). Autentikasi Node.js Express: Konsep, Metode, dan Contoh. https://apidog.com
- DevTo (2025). OAuth Authentication: Google, Facebook, Github Login via Node JS (Banckend). https://dev.to/uniyalmanas
- Medium (2023). Understanding Hashing, Encoding, and Encryption in Express.js. https://medium.com

0 Komentar