Back to Blog

Praktik Terbaik Keamanan API

Pola keamanan esensial untuk melindungi API Anda dari serangan umum, dengan contoh implementasi praktis untuk tim development.

Mengapa Keamanan API Perlu Perhatian Khusus

API adalah pintu depan ke data Anda. Setiap aplikasi mobile, single-page application, integrasi pihak ketiga, dan microservice internal berkomunikasi melalui API. Kerentanan di API Anda mengekspos bukan hanya satu interface, tapi berpotensi setiap klien yang terhubung ke sana.

OWASP API Security Top 10 mencantumkan kerentanan API paling umum: broken object-level authorization, broken authentication, excessive data exposure, lack of rate limiting, dan lainnya. Ini bukan risiko teoretis. Ini adalah pola serangan yang digunakan dalam pelanggaran nyata setiap minggu.

Panduan ini mencakup pola keamanan praktis yang harus diimplementasikan setiap tim development.

Autentikasi

Autentikasi Berbasis Token

Session cookie bekerja untuk aplikasi web tradisional, tapi API yang melayani aplikasi mobile, SPA, dan klien pihak ketiga membutuhkan autentikasi berbasis token. JSON Web Token (JWT) adalah standarnya:

// Struktur token
// Header: algoritma dan tipe token
// Payload: claims (data user, izin, kadaluarsa)
// Signature: verifikasi kriptografi

const token = jwt.sign(
  {
    sub: user.id,
    email: user.email,
    roles: ['admin', 'editor'],
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + (60 * 15) // 15 menit
  },
  process.env.JWT_SECRET,
  { algorithm: 'HS256' }
);

Praktik JWT kritis:

  • Kadaluarsa pendek: Access token harus kadaluarsa dalam 15 hingga 30 menit
  • Refresh token: Gunakan refresh token berumur lebih panjang yang disimpan dengan aman untuk mendapatkan access token baru
  • Secret yang kuat: Gunakan kunci minimal 256-bit. Jangan pernah hardcode.
  • Validasi algoritma: Selalu tentukan algoritma yang diharapkan untuk mencegah serangan kebingungan algoritma

OAuth 2.0 untuk Akses Pihak Ketiga

Saat aplikasi eksternal membutuhkan akses ke API Anda, gunakan OAuth 2.0 dengan authorization code flow:

1. Klien mengarahkan user ke authorization server
2. User mengautentikasi dan memberikan izin
3. Authorization server mengembalikan authorization code
4. Klien menukar code dengan access token (server-to-server)
5. Klien menggunakan access token untuk memanggil API

Jangan pernah gunakan implicit flow untuk aplikasi baru. Ini mengekspos token di URL dan riwayat browser.

API Key untuk Service-to-Service

Untuk komunikasi layanan internal atau integrasi pihak ketiga terpercaya:

  • Hasilkan API key acak secara kriptografis (minimal 32 byte)
  • Hash API key yang disimpan (jangan pernah simpan dalam plain text)
  • Dukung rotasi kunci tanpa downtime
  • Ikat kunci ke range IP atau layanan tertentu jika memungkinkan
  • Log semua penggunaan API key untuk audit

Otorisasi

Otorisasi Level Objek

Kerentanan API paling umum: mengecek apakah user terotentikasi tapi tidak apakah mereka diotorisasi untuk mengakses resource tertentu.

// SALAH: Hanya mengecek autentikasi
app.get('/api/orders/:id', authenticate, async (req, res) => {
  const order = await Order.findById(req.params.id);
  res.json(order); // User terotentikasi manapun bisa melihat pesanan apapun
});

// BENAR: Mengecek otorisasi level objek
app.get('/api/orders/:id', authenticate, async (req, res) => {
  const order = await Order.findById(req.params.id);
  
  if (!order) {
    return res.status(404).json({ error: 'Pesanan tidak ditemukan' });
  }
  
  if (order.userId !== req.user.id && !req.user.roles.includes('admin')) {
    return res.status(403).json({ error: 'Akses ditolak' });
  }
  
  res.json(order);
});

Setiap endpoint yang mengakses resource tertentu harus memverifikasi bahwa user yang terotentikasi memiliki izin untuk mengakses resource tersebut.

Otorisasi Level Fungsi

Pastikan operasi khusus admin benar-benar dibatasi untuk admin:

function requireRole(...roles) {
  return (req, res, next) => {
    if (!roles.some(role => req.user.roles.includes(role))) {
      return res.status(403).json({ error: 'Izin tidak cukup' });
    }
    next();
  };
}

app.delete('/api/users/:id', authenticate, requireRole('admin'), async (req, res) => {
  // Hanya admin yang mencapai kode ini
});

Kontrol Akses Level Field

Tidak setiap user harus melihat setiap field dalam respons:

function filterUserResponse(user, requestingUser) {
  const publicFields = {
    id: user.id,
    name: user.name,
    department: user.department
  };

  if (requestingUser.roles.includes('hr') || requestingUser.id === user.id) {
    return {
      ...publicFields,
      email: user.email,
      phone: user.phone,
      salary: requestingUser.roles.includes('hr') ? user.salary : undefined
    };
  }

  return publicFields;
}

Validasi Input

Validasi Segalanya

Jangan pernah percaya input klien. Validasi tipe, format, panjang, dan range untuk setiap parameter:

const { z } = require('zod');

const createOrderSchema = z.object({
  customerId: z.string().uuid(),
  items: z.array(z.object({
    productId: z.string().uuid(),
    quantity: z.number().int().positive().max(10000),
    unitPrice: z.number().positive().max(999999999)
  })).min(1).max(100),
  deliveryDate: z.string().datetime(),
  notes: z.string().max(500).optional()
});

app.post('/api/orders', authenticate, async (req, res) => {
  const result = createOrderSchema.safeParse(req.body);
  
  if (!result.success) {
    return res.status(400).json({
      error: 'Validasi gagal',
      details: result.error.issues
    });
  }
  
  // Proses data yang tervalidasi
  const order = await createOrder(result.data);
  res.status(201).json(order);
});

Pencegahan SQL Injection

Selalu gunakan parameterized query. Jangan pernah konkatenasi input user ke string SQL:

// BERBAHAYA: Kerentanan SQL injection
const query = `SELECT * FROM users WHERE email = '${req.body.email}'`;

// AMAN: Parameterized query
const query = 'SELECT * FROM users WHERE email = $1';
const result = await db.query(query, [req.body.email]);

Rate Limiting

Lindungi dari Penyalahgunaan

Tanpa rate limiting, satu klien bisa membanjiri API Anda dengan request, baik melalui serangan yang disengaja maupun kode yang buggy:

const rateLimit = require('express-rate-limit');

// Rate limit umum API
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 menit
  max: 100, // 100 request per window
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Terlalu banyak request, silakan coba lagi nanti' }
});

// Limit lebih ketat untuk endpoint autentikasi
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5, // 5 percobaan per 15 menit
  message: { error: 'Terlalu banyak percobaan login' }
});

app.use('/api/', apiLimiter);
app.use('/api/auth/', authLimiter);

Rate Limiting Bertingkat

Terapkan limit berbeda berdasarkan status autentikasi dan sensitivitas endpoint:

Tipe EndpointAnonimTerotentikasiPremium
Read publik30/mnt100/mnt500/mnt
Read terotentikasiN/A60/mnt300/mnt
Operasi writeN/A20/mnt100/mnt
Autentikasi5/15mntN/AN/A
Operasi adminN/A30/mnt30/mnt

Keamanan Respons

Minimalkan Eksposur Data

Kembalikan hanya field yang dibutuhkan klien. Jangan pernah kembalikan seluruh record database:

// BURUK: Mengembalikan segalanya termasuk field sensitif
app.get('/api/users/:id', async (req, res) => {
  const user = await User.findById(req.params.id);
  res.json(user); // Termasuk hash password, catatan internal, dll.
});

// BAIK: Mengembalikan hanya field yang diperlukan
app.get('/api/users/:id', async (req, res) => {
  const user = await User.findById(req.params.id)
    .select('id name email department createdAt');
  res.json(user);
});

Header Keamanan

Set header keamanan HTTP yang sesuai di semua respons API:

app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('Cache-Control', 'no-store');
  res.setHeader('Content-Security-Policy', "default-src 'none'");
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  next();
});

Penanganan Error

Jangan pernah ekspos detail internal dalam respons error:

// BURUK: Membocorkan detail implementasi
res.status(500).json({
  error: 'PostgreSQL error: relation "users" does not exist',
  stack: error.stack
});

// BAIK: Pesan generik dengan correlation ID untuk debugging
const correlationId = crypto.randomUUID();
logger.error({ correlationId, error: error.message, stack: error.stack });
res.status(500).json({
  error: 'Terjadi kesalahan internal',
  correlationId: correlationId
});

Logging dan Monitoring

Apa yang Harus Di-Log

Setiap request API harus menghasilkan entri log yang berisi:

  • Timestamp
  • Method dan path request
  • Alamat IP klien dan user agent
  • ID user terotentikasi (jika berlaku)
  • Status code respons
  • Response time
  • Request correlation ID

Apa yang Harus Di-Alert

Setup alert otomatis untuk:

  • Lonjakan kegagalan autentikasi (potensi brute force)
  • Pelanggaran rate limit (potensi serangan atau klien yang bermasalah)
  • Lonjakan respons 403 (potensi probing otorisasi)
  • Peningkatan error rate 500 (potensi exploit atau masalah sistem)
  • Pola traffic tidak biasa (volume, asal geografis, waktu)

Checklist Keamanan untuk Setiap API

Sebelum men-deploy endpoint API manapun:

  • Autentikasi diperlukan untuk endpoint non-publik
  • Pengecekan otorisasi level objek di setiap akses resource
  • Validasi input untuk semua parameter (tipe, format, panjang, range)
  • Rate limiting dikonfigurasi dengan tepat
  • Field respons diminimalkan hanya data yang diperlukan
  • Pesan error tidak membocorkan detail internal
  • Header keamanan di-set
  • HTTPS ditegakkan (tanpa HTTP)
  • Logging menangkap event yang relevan keamanan
  • Pencegahan SQL injection melalui parameterized query

Keamanan API bukan proyek satu kali. Ini adalah praktik berkelanjutan yang harus menjadi bagian dari setiap code review, setiap deployment, dan setiap keputusan arsitektur. Biaya untuk melakukannya dengan benar itu kecil. Biaya untuk melakukannya dengan salah bisa menghancurkan.

Read in English English Version