Web Development

Authentication & Authorization untuk Web

Panduan lengkap memahami authentication dan authorization β€” session vs JWT, OAuth 2.0, RBAC, password hashing, 2FA, dan security best practices dengan contoh kode

1. Pengenalan Authentication & Authorization

Dalam pengembangan web, authentication dan authorization adalah dua konsep keamanan yang berbeda namun saling melengkapi. Memahami perbedaannya sangat penting untuk membangun aplikasi web yang aman.

Authentication vs Authorization

Aspek Authentication (Autentikasi) Authorization (Otorisasi)
Pertanyaan"Siapa kamu?""Apa yang boleh kamu lakukan?"
TujuanMemverifikasi identitas penggunaMenentukan hak akses pengguna
KapanSebelum authorizationSetelah authentication
ContohLogin dengan email & passwordAdmin bisa hapus user, user biasa tidak
DataCredential (password, token, biometric)Permissions, roles, policies
Diagram: Alur Authentication & Authorization
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚            ALUR AUTHENTICATION & AUTHORIZATION                β”‚
β”‚                                                                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚  User    │───►│ Authentication│───►│   Authorization      β”‚ β”‚
β”‚  β”‚ Request  β”‚    β”‚  (Siapa?)     β”‚    β”‚   (Boleh apa?)       β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚                         β”‚                        β”‚             β”‚
β”‚                    β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”              β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”        β”‚
β”‚                    β”‚ Verifikasiβ”‚             β”‚ Cek Roleβ”‚        β”‚
β”‚                    β”‚ Credentialβ”‚             β”‚ & Perm. β”‚        β”‚
β”‚                    β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜              β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜        β”‚
β”‚                         β”‚                        β”‚             β”‚
β”‚                  β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”     β”‚
β”‚                  β”‚ βœ… Berhasil  β”‚          β”‚ βœ… Diizinkan β”‚     β”‚
β”‚                  β”‚ ❌ Gagal     β”‚          β”‚ ❌ Ditolak   β”‚     β”‚
β”‚                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Metode Authentication yang Umum

Metode Cara Kerja Cocok Untuk
Session-basedServer menyimpan session, client menyimpan cookieWeb tradisional, monolith app
Token-based (JWT)Server mengeluarkan token, client menyimpannyaSPA, mobile app, API
OAuth 2.0Delegasi autentikasi ke provider (Google, GitHub)Login sosial, integrasi pihak ketiga
API KeyKode unik untuk mengidentifikasi aplikasiAPI publik, server-to-server
BiometricSidik jari, Face ID, iris scanAplikasi mobile high-security

2. Session-based Authentication

Session-based authentication adalah metode autentikasi tradisional di mana server menyimpan informasi session pengguna. Saat pengguna login, server membuat session dan mengirim session ID ke browser melalui cookie.

Cara Kerja Session

Diagram: Alur Session Authentication
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Browser    β”‚          β”‚   Server    β”‚         β”‚   Database   β”‚
β”‚              β”‚  1. POST β”‚             β”‚         β”‚              β”‚
β”‚  Login Form  │─────────►│  /login     │────────►│  Cek user    β”‚
β”‚  email+pass  β”‚          β”‚             β”‚         β”‚  di database β”‚
β”‚              β”‚          β”‚             │◄────────│              β”‚
β”‚              │◄─────────│  Set-Cookie β”‚         β”‚              β”‚
β”‚              β”‚  2.      β”‚  session_id β”‚         β”‚              β”‚
β”‚              β”‚  Cookie  β”‚  =abc123    β”‚         β”‚              β”‚
β”‚              β”‚          β”‚             β”‚         β”‚              β”‚
β”‚              β”‚  3. GET  β”‚             β”‚         β”‚              β”‚
β”‚              │─────────►│  /dashboard │────────►│  Ambil data  β”‚
β”‚  Cookie:     β”‚  Cookie  β”‚  Cek sessionβ”‚         β”‚  berdasarkan β”‚
β”‚  session=abc β”‚          β”‚  abc123 βœ…  β”‚         β”‚  user_id     β”‚
β”‚              │◄─────────│  Response   │◄────────│              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Implementasi Session dengan Express.js

JavaScript β€” Express.js Session
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const bcrypt = require('bcrypt');

const app = express();
app.use(express.json());

// Setup Redis client untuk session store
const redisClient = createClient({
  url: 'redis://localhost:6379'
});
redisClient.connect();

// Konfigurasi session
app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,  // Gunakan secret yang kuat
  name: 'sessionId',  // Jangan gunakan default 'connect.sid'
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,     // Tidak bisa diakses JavaScript
    secure: true,       // Hanya dikirim via HTTPS
    sameSite: 'strict', // Mencegah CSRF
    maxAge: 15 * 60 * 1000, // 15 menit
    path: '/',
  }
}));

// Middleware: cek autentikasi
function requireAuth(req, res, next) {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Silakan login terlebih dahulu' });
  }
  next();
}

// Middleware: cek role
function requireRole(...roles) {
  return (req, res, next) => {
    if (!roles.includes(req.session.role)) {
      return res.status(403).json({ error: 'Akses ditolak' });
    }
    next();
  };
}

// Endpoint: Login
app.post('/api/login', async (req, res) => {
  const { email, password } = req.body;

  // Cari user di database
  const user = await db.query('SELECT * FROM users WHERE email = $1', [email]);
  if (!user.rows[0]) {
    return res.status(401).json({ error: 'Email atau password salah' });
  }

  // Verifikasi password
  const validPassword = await bcrypt.compare(password, user.rows[0].password_hash);
  if (!validPassword) {
    return res.status(401).json({ error: 'Email atau password salah' });
  }

  // Simpan data di session
  req.session.userId = user.rows[0].id;
  req.session.email = user.rows[0].email;
  req.session.role = user.rows[0].role;

  res.json({ message: 'Login berhasil', user: { email: user.rows[0].email } });
});

// Endpoint: Logout
app.post('/api/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) return res.status(500).json({ error: 'Gagal logout' });
    res.clearCookie('sessionId');
    res.json({ message: 'Logout berhasil' });
  });
});

// Endpoint terproteksi
app.get('/api/dashboard', requireAuth, (req, res) => {
  res.json({ message: `Selamat datang, user ${req.session.userId}` });
});

// Endpoint dengan role check
app.delete('/api/users/:id', requireAuth, requireRole('admin'), async (req, res) => {
  await db.query('DELETE FROM users WHERE id = $1', [req.params.id]);
  res.json({ message: 'User berhasil dihapus' });
});

app.listen(3000);
πŸ’‘ Session Store

Jangan menyimpan session di memory server (default Express). Gunakan Redis, PostgreSQL, atau MongoDB sebagai session store. Memory server akan hilang saat restart dan tidak scalable untuk multiple server instances.

3. JSON Web Token (JWT)

JWT (JSON Web Token) adalah standar terbuka (RFC 7519) untuk membuat token autentikasi yang aman dan self-contained. JWT berisi informasi terkait pengguna yang dapat diverifikasi tanpa mengakses database.

Struktur JWT

Text β€” Struktur JWT
// JWT terdiri dari 3 bagian yang dipisahkan oleh titik (.)
// HEADER.PAYLOAD.SIGNATURE

// 1. HEADER β€” Algoritma dan tipe token
{
  "alg": "HS256",    // Algoritma signing (HS256 atau RS256)
  "typ": "JWT"       // Tipe token
}
// Base64Url: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

// 2. PAYLOAD β€” Data (claims) yang disimpan
{
  "sub": "1234567890",         // Subject (user ID)
  "name": "Budi Santoso",     // Nama pengguna
  "email": "budi@mail.com",   // Email
  "role": "admin",            // Role pengguna
  "iat": 1719340800,          // Issued At (waktu dibuat)
  "exp": 1719344400           // Expiration (waktu kedaluwarsa)
}
// Base64Url: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6I...

// 3. SIGNATURE β€” Tanda tangan digital untuk verifikasi
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  SECRET_KEY
)
// Base64Url: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

// Hasil akhir:
// eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature

Session vs JWT

Aspek Session JWT
PenyimpananServer-side (database/Redis)Client-side (localStorage/cookie)
ScalabilityPerlu shared session storeStateless, mudah di-scale
RevocationHapus session di serverSulit (perlu blacklist)
UkuranHanya session ID di cookieToken bisa besar (payload panjang)
CORSOtomatis dengan cookiePerlu header manual
Cocok untukMonolith, web tradisionalSPA, mobile, microservices

Implementasi JWT dengan Node.js

JavaScript β€” JWT Implementation
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

// Secret keys β€” simpan di environment variables!
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;

// Generate access token (berlaku 15 menit)
function generateAccessToken(user) {
  return jwt.sign(
    {
      sub: user.id,
      email: user.email,
      role: user.role,
    },
    ACCESS_SECRET,
    { expiresIn: '15m' }
  );
}

// Generate refresh token (berlaku 7 hari)
function generateRefreshToken(user) {
  return jwt.sign(
    { sub: user.id },
    REFRESH_SECRET,
    { expiresIn: '7d' }
  );
}

// Middleware: verifikasi access token
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

  if (!token) {
    return res.status(401).json({ error: 'Token tidak ditemukan' });
  }

  try {
    const decoded = jwt.verify(token, ACCESS_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token kedaluwarsa', code: 'TOKEN_EXPIRED' });
    }
    return res.status(403).json({ error: 'Token tidak valid' });
  }
}

// Endpoint: Login β€” mengembalikan kedua token
app.post('/api/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await findUserByEmail(email);

  if (!user || !await bcrypt.compare(password, user.passwordHash)) {
    return res.status(401).json({ error: 'Kredensial salah' });
  }

  const accessToken = generateAccessToken(user);
  const refreshToken = generateRefreshToken(user);

  // Simpan refresh token di database
  await db.query(
    'UPDATE users SET refresh_token = $1 WHERE id = $2',
    [refreshToken, user.id]
  );

  // Kirim refresh token sebagai httpOnly cookie
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000 // 7 hari
  });

  res.json({
    accessToken,
    user: { id: user.id, email: user.email, role: user.role }
  });
});

// Endpoint: Refresh token
app.post('/api/refresh', async (req, res) => {
  const refreshToken = req.cookies.refreshToken;

  if (!refreshToken) {
    return res.status(401).json({ error: 'Refresh token tidak ditemukan' });
  }

  try {
    const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
    const user = await db.query(
      'SELECT * FROM users WHERE id = $1 AND refresh_token = $2',
      [decoded.sub, refreshToken]
    );

    if (!user.rows[0]) {
      return res.status(403).json({ error: 'Refresh token tidak valid' });
    }

    const newAccessToken = generateAccessToken(user.rows[0]);
    res.json({ accessToken: newAccessToken });
  } catch (error) {
    return res.status(403).json({ error: 'Refresh token tidak valid' });
  }
});

// Endpoint terproteksi
app.get('/api/profile', authenticateToken, (req, res) => {
  res.json({
    message: 'Data profil',
    userId: req.user.sub,
    email: req.user.email,
    role: req.user.role
  });
});
⚠️ Keamanan JWT

Jangan menyimpan data sensitif di payload JWT β€” payload hanya di-encode (Base64), bukan dienkripsi. Siapa pun bisa membaca isinya. Juga, simpan secret key yang sangat kuat dan jangan pernah commit ke repository. Gunakan environment variables atau secret manager.

4. OAuth 2.0

OAuth 2.0 adalah protokol standar industri untuk delegasi akses. OAuth memungkinkan pengguna memberikan akses terbatas ke resource mereka di satu aplikasi ke aplikasi lain, tanpa membagikan password.

Alur OAuth 2.0 β€” Authorization Code Flow

Diagram: OAuth 2.0 Authorization Code Flow
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  User    β”‚     β”‚  Aplikasi    β”‚     β”‚  Auth Server β”‚    β”‚  Resource  β”‚
β”‚ (Browser)β”‚     β”‚  (Client)    β”‚     β”‚  (Google)    β”‚    β”‚  Server    β”‚
β”‚          β”‚     β”‚              β”‚     β”‚              β”‚    β”‚            β”‚
β”‚  1. Klik │────►│              β”‚     β”‚              β”‚    β”‚            β”‚
β”‚  "Login  β”‚     β”‚  2. Redirect │────►│  3. User     β”‚    β”‚            β”‚
β”‚  Google" β”‚     β”‚  ke Google   β”‚     β”‚  Login &     β”‚    β”‚            β”‚
β”‚          β”‚     β”‚              β”‚     β”‚  Grant Accessβ”‚    β”‚            β”‚
β”‚          │◄────│              │◄────│  4. Redirect β”‚    β”‚            β”‚
β”‚          β”‚  5. β”‚  Auth Code   β”‚     β”‚  + Auth Code β”‚    β”‚            β”‚
β”‚          β”‚     β”‚              β”‚     β”‚              β”‚    β”‚            β”‚
β”‚          β”‚     β”‚  6. Tukar    │────►│  7. Verifikasiβ”‚   β”‚            β”‚
β”‚          β”‚     β”‚  Code +      β”‚     β”‚  & kirim     β”‚    β”‚            β”‚
β”‚          β”‚     β”‚  Client Secretβ”‚    β”‚  Access Tokenβ”‚    β”‚            β”‚
β”‚          β”‚     β”‚              │◄────│  + Refresh   β”‚    β”‚            β”‚
β”‚          β”‚     β”‚  8. Simpan   β”‚     β”‚              β”‚    β”‚            β”‚
β”‚          β”‚     β”‚  Token       β”‚     β”‚              β”‚    β”‚            β”‚
β”‚          β”‚     β”‚  9. Akses API│─────────────────────────►│  10. Data β”‚
β”‚          │◄────│  11. Response│◄────────────────────────│            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Implementasi OAuth 2.0 dengan Passport.js

JavaScript β€” OAuth 2.0 Google dengan Passport.js
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;

// Konfigurasi Google OAuth Strategy
passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: '/api/auth/google/callback',
    scope: ['profile', 'email']
  },
  async (accessToken, refreshToken, profile, done) => {
    try {
      // Cek apakah user sudah ada di database
      let user = await db.query(
        'SELECT * FROM users WHERE google_id = $1',
        [profile.id]
      );

      if (user.rows[0]) {
        // User sudah ada β€” update last login
        await db.query(
          'UPDATE users SET last_login = NOW() WHERE google_id = $1',
          [profile.id]
        );
        return done(null, user.rows[0]);
      }

      // User baru β€” buat akun
      const newUser = await db.query(
        `INSERT INTO users (google_id, email, name, avatar, provider)
         VALUES ($1, $2, $3, $4, 'google')
         RETURNING *`,
        [profile.id, profile.emails[0].value, profile.displayName, profile.photos[0].value]
      );

      return done(null, newUser.rows[0]);
    } catch (error) {
      return done(error, null);
    }
  }
));

// Serialize/Deserialize user untuk session
passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser(async (id, done) => {
  const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
  done(null, user.rows[0]);
});

// Routes
app.get('/api/auth/google',
  passport.authenticate('google', { scope: ['profile', 'email'] })
);

app.get('/api/auth/google/callback',
  passport.authenticate('google', { failureRedirect: '/login' }),
  (req, res) => {
    // Autentikasi berhasil
    res.redirect('/dashboard');
  }
);

app.get('/api/auth/logout', (req, res) => {
  req.logout(() => {
    res.redirect('/');
  });
});

// Cek status autentikasi
app.get('/api/auth/status', (req, res) => {
  if (req.isAuthenticated()) {
    res.json({ authenticated: true, user: req.user });
  } else {
    res.json({ authenticated: false });
  }
});
πŸ’‘ Tips OAuth

Untuk membuat Google OAuth credentials, kunjungi Google Cloud Console β†’ APIs & Services β†’ Credentials β†’ Create OAuth 2.0 Client ID. Pastikan redirect URI yang dikonfigurasi sesuai dengan callback URL di aplikasi Anda.

5. Role-Based Access Control (RBAC)

RBAC adalah metode authorization yang mengelompokkan pengguna ke dalam roles dan memberikan permissions berdasarkan role tersebut. Ini menyederhanakan manajemen akses dibanding memberikan izin ke setiap pengguna secara individual.

Struktur RBAC

Diagram: Struktur RBAC
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   RBAC STRUCTURE                     β”‚
β”‚                                                      β”‚
β”‚   USERS            ROLES           PERMISSIONS      β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚   β”‚ Budi  │──────►│ Admin  │──────►│ users:read   β”‚  β”‚
β”‚   β”‚        β”‚      β”‚        β”‚      β”‚ users:write  β”‚  β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚        β”‚      β”‚ users:delete  β”‚  β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚        β”‚      β”‚ posts:manage β”‚  β”‚
β”‚   β”‚ Sari  │──────►│        β”‚      β”‚ settings:all β”‚  β”‚
β”‚   β”‚        β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚ Editor │──────►│ posts:write  β”‚  β”‚
β”‚   β”‚ Andi  │──────►│        β”‚      β”‚ posts:read   β”‚  β”‚
β”‚   β”‚        β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚ media:upload β”‚  β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                   β”‚ Viewer β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚                   β”‚        │──────►│ posts:read   β”‚  β”‚
β”‚                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚ profile:edit β”‚  β”‚
β”‚                                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Implementasi RBAC

JavaScript β€” RBAC Implementation
// Definisi roles dan permissions
const ROLES = {
  admin: {
    permissions: [
      'users:read', 'users:write', 'users:delete',
      'posts:read', 'posts:write', 'posts:delete', 'posts:publish',
      'media:read', 'media:upload', 'media:delete',
      'settings:read', 'settings:write',
      'analytics:read',
    ]
  },
  editor: {
    permissions: [
      'posts:read', 'posts:write', 'posts:publish',
      'media:read', 'media:upload',
      'analytics:read',
    ]
  },
  author: {
    permissions: [
      'posts:read', 'posts:write',
      'media:read', 'media:upload',
    ]
  },
  viewer: {
    permissions: [
      'posts:read',
      'profile:read', 'profile:write',
    ]
  }
};

// Middleware: cek permission
function checkPermission(requiredPermission) {
  return (req, res, next) => {
    const userRole = req.user.role;
    const roleConfig = ROLES[userRole];

    if (!roleConfig) {
      return res.status(403).json({ error: 'Role tidak dikenal' });
    }

    if (!roleConfig.permissions.includes(requiredPermission)) {
      return res.status(403).json({
        error: 'Akses ditolak',
        required: requiredPermission,
        yourRole: userRole
      });
    }

    next();
  };
}

// Middleware: cek multiple permissions (harus punya semua)
function checkAllPermissions(...permissions) {
  return (req, res, next) => {
    const roleConfig = ROLES[req.user.role];
    const hasAll = permissions.every(p =>
      roleConfig.permissions.includes(p)
    );

    if (!hasAll) {
      return res.status(403).json({ error: 'Akses ditolak' });
    }
    next();
  };
}

// Menggunakan middleware
app.get('/api/users', checkPermission('users:read'), getUsers);
app.post('/api/users', checkPermission('users:write'), createUser);
app.delete('/api/users/:id', checkPermission('users:delete'), deleteUser);

app.put('/api/posts/:id', checkPermission('posts:write'), updatePost);
app.post('/api/posts/:id/publish',
  checkAllPermissions('posts:write', 'posts:publish'),
  publishPost
);

6. Password Hashing

Menyimpan password dalam bentuk plaintext adalah kesalahan fatal. Password harus selalu di-hash menggunakan algoritma yang aman sebelum disimpan ke database. Hashing adalah proses satu arah β€” tidak bisa dikembalikan ke password asli.

Algoritma Hashing yang Aman

Algoritma Keamanan Rekomendasi
bcryptβœ… Sangat baik β€” adaptive cost⭐ Rekomendasi utama untuk web
argon2idβœ… Terbaik β€” pemenang PHC⭐ Pilihan modern terbaik
scryptβœ… Baik β€” memory-hardAlternatif yang aman
SHA-256❌ Buruk β€” terlalu cepatJANGAN gunakan untuk password
MD5❌ Sangat buruk β€” sudah dipecahkanJANGAN PERNAH gunakan
JavaScript β€” Password Hashing dengan bcrypt
const bcrypt = require('bcrypt');

// Konfigurasi
const SALT_ROUNDS = 12; // Semakin tinggi semakin aman, tapi lebih lambat

// Hash password saat registrasi
async function registerUser(email, plainPassword) {
  // Validasi password strength
  if (plainPassword.length < 8) {
    throw new Error('Password minimal 8 karakter');
  }

  // Hash password
  const passwordHash = await bcrypt.hash(plainPassword, SALT_ROUNDS);
  // Hasil: $2b$12$LJ3m4ys1Ly...

  // Simpan ke database (JANGAN simpan plainPassword!)
  const result = await db.query(
    'INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING id',
    [email, passwordHash]
  );

  return result.rows[0];
}

// Verifikasi password saat login
async function loginUser(email, plainPassword) {
  const user = await db.query(
    'SELECT id, email, password_hash FROM users WHERE email = $1',
    [email]
  );

  if (!user.rows[0]) {
    // Gunakan pesan error yang sama untuk mencegah user enumeration
    throw new Error('Email atau password salah');
  }

  // bcrypt.compare menangani salt secara otomatis
  const isValid = await bcrypt.compare(plainPassword, user.rows[0].password_hash);

  if (!isValid) {
    throw new Error('Email atau password salah');
  }

  return user.rows[0];
}

// Password strength checker
function checkPasswordStrength(password) {
  const checks = {
    length: password.length >= 8,
    uppercase: /[A-Z]/.test(password),
    lowercase: /[a-z]/.test(password),
    number: /[0-9]/.test(password),
    special: /[!@#$%^&*(),.?":{}|<>]/.test(password),
  };

  const score = Object.values(checks).filter(Boolean).length;

  return {
    score,        // 0-5
    checks,
    strong: score >= 4,
    message: score >= 4 ? 'Password kuat βœ…' : 'Password lemah β€” gunakan kombinasi huruf, angka, simbol'
  };
}
⚠️ Jangan Pernah Lakukan Ini!
  • Menyimpan password dalam bentuk plaintext
  • Menggunakan MD5 atau SHA-1 untuk hashing password
  • Mengirim password melalui URL query parameters
  • Menyimpan password di log server atau error messages
  • Menggunakan salt yang sama untuk semua password (bcrypt sudah handle ini)

7. Two-Factor Authentication (2FA)

Two-Factor Authentication (2FA) menambahkan lapisan keamanan ekstra dengan meminta dua bentuk verifikasi: sesuatu yang Anda tahu (password) dan sesuatu yang Anda miliki (kode dari aplikasi authenticator atau SMS).

Jenis 2FA

Jenis Cara Kerja Keamanan
TOTP (Time-based OTP)Kode 6 digit dari Google Authenticator / Authy⭐⭐⭐⭐ Sangat baik
SMS OTPKode dikirim via SMS⭐⭐ Sedang (bisa di-SS7 attack)
Email OTPKode dikirim via email⭐⭐ Sedang
WebAuthn / PasskeyHardware key atau biometric⭐⭐⭐⭐⭐ Terbaik

Implementasi TOTP 2FA

JavaScript β€” TOTP 2FA dengan speakeasy
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');

// Setup 2FA untuk user baru
async function setup2FA(userId) {
  // Generate secret unik untuk user
  const secret = speakeasy.generateSecret({
    name: 'BeebaneLabs',           // Nama aplikasi
    account: 'user@email.com',     // Email user
    length: 32
  });

  // Simpan secret di database (encrypted!)
  await db.query(
    'UPDATE users SET twofa_secret = $1, twofa_enabled = false WHERE id = $2',
    [encrypt(secret.base32), userId]
  );

  // Generate QR code untuk di-scan
  const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);

  return {
    secret: secret.base32,   // Backup: user bisa simpan manual
    qrCode: qrCodeUrl,      // QR code untuk Google Authenticator
    message: 'Scan QR code dengan Google Authenticator'
  };
}

// Verifikasi kode 2FA saat login
function verify2FA(secret, token) {
  return speakeasy.totp.verify({
    secret: secret,
    encoding: 'base32',
    token: token,
    window: 1  // Izinkan 1 step tolerance (30 detik)
  });
}

// Endpoint: Setup 2FA
app.post('/api/2fa/setup', authenticateToken, async (req, res) => {
  const result = await setup2FA(req.user.sub);
  res.json(result);
});

// Endpoint: Verifikasi & aktifkan 2FA
app.post('/api/2fa/verify', authenticateToken, async (req, res) => {
  const { token } = req.body;
  const user = await db.query('SELECT * FROM users WHERE id = $1', [req.user.sub]);

  const secret = decrypt(user.rows[0].twofa_secret);
  const isValid = verify2FA(secret, token);

  if (!isValid) {
    return res.status(400).json({ error: 'Kode 2FA tidak valid' });
  }

  // Aktifkan 2FA
  await db.query('UPDATE users SET twofa_enabled = true WHERE id = $1', [req.user.sub]);

  // Generate backup codes
  const backupCodes = Array.from({ length: 8 }, () =>
    speakeasy.generateSecret({ length: 10 }).base32.slice(0, 8)
  );

  await db.query(
    'UPDATE users SET twofa_backup_codes = $1 WHERE id = $2',
    [JSON.stringify(backupCodes.map(c => hash(c))), req.user.sub]
  );

  res.json({
    message: '2FA berhasil diaktifkan βœ…',
    backupCodes,
    warning: 'Simpan backup codes ini β€” Anda hanya bisa melihatnya sekali!'
  });
});

// Login dengan 2FA
app.post('/api/login-2fa', async (req, res) => {
  const { email, password, totpToken } = req.body;

  // Step 1: Verifikasi email & password
  const user = await loginUser(email, password);

  // Step 2: Jika 2FA aktif, verifikasi TOTP
  if (user.twofa_enabled) {
    if (!totpToken) {
      return res.status(200).json({ require2FA: true, message: 'Masukkan kode 2FA' });
    }

    const secret = decrypt(user.twofa_secret);
    if (!verify2FA(secret, totpToken)) {
      return res.status(401).json({ error: 'Kode 2FA salah' });
    }
  }

  // Step 3: Generate token
  const accessToken = generateAccessToken(user);
  res.json({ accessToken, user: { id: user.id, email: user.email } });
});

8. Security Best Practices

Berikut best practices keamanan yang harus diterapkan di setiap aplikasi web production:

HTTP Security Headers

JavaScript β€” Security Headers dengan Helmet.js
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const cors = require('cors');

// Security headers
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'", "https://api.example.com"],
    }
  },
  hsts: { maxAge: 31536000, includeSubDomains: true },
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
}));

// CORS configuration
app.use(cors({
  origin: ['https://beebanelabs.pages.dev', 'http://localhost:3000'],
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
}));

// Rate limiting β€” mencegah brute force
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 menit
  max: 5,                     // Maks 5 attempt
  message: { error: 'Terlalu banyak percobaan login. Coba lagi dalam 15 menit.' },
  standardHeaders: true,
});

app.use('/api/login', loginLimiter);

// General API rate limit
const apiLimiter = rateLimit({
  windowMs: 1 * 60 * 1000,  // 1 menit
  max: 100,                   // Maks 100 request per menit
});
app.use('/api/', apiLimiter);
βœ… Security Checklist
  • Selalu gunakan HTTPS di production
  • Terapkan rate limiting pada endpoint login dan API
  • Gunakan httpOnly cookies untuk token sensitif
  • Validasi dan sanitize semua input pengguna
  • Gunakan CSP headers untuk mencegah XSS
  • Implementasikan CSRF protection untuk form
  • Enkripsi data sensitif at rest dan in transit
  • Lakukan security audit secara berkala
  • Log semua aktivitas autentikasi untuk monitoring
  • Gunakan dependency scanning (npm audit, Snyk)

9. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Authentication & Authorization:

Pertanyaan 1: Apa perbedaan utama antara authentication dan authorization?

a) Authentication mengenkripsi data, authorization mendekripsinya
b) Authentication memverifikasi identitas ("siapa kamu?"), authorization menentukan hak akses ("boleh apa?")
c) Authentication untuk admin, authorization untuk user biasa
d) Tidak ada perbedaan β€” keduanya sama

Pertanyaan 2: Mengapa JWT cocok untuk SPA dan mobile app dibanding session?

a) JWT lebih aman dari session
b) JWT bersifat stateless β€” tidak perlu menyimpan session di server, mudah di-scale
c) JWT tidak bisa kedaluwarsa
d) JWT hanya untuk API, session hanya untuk web

Pertanyaan 3: Algoritma hashing apa yang direkomendasikan untuk menyimpan password?

a) MD5
b) SHA-256
c) bcrypt atau argon2id
d) Base64 encoding

Pertanyaan 4: Apa yang dilakukan OAuth 2.0?

a) Menggantikan password dengan biometric
b) Mengenkripsi seluruh database
c) Memungkinkan delegasi akses tanpa membagikan password ke aplikasi pihak ketiga
d) Menghapus cookie otomatis

Pertanyaan 5: Dalam RBAC, apa yang dimaksud dengan "permission"?

a) Password pengguna
b) Hak akses spesifik (misal: users:read, posts:write) yang diberikan kepada role
c) Jumlah login yang diperbolehkan
d) Token JWT yang diterima pengguna