DEV Community

Ai Code
Ai Code

Posted on • Originally published at savefilearchive.blogspot.com

Panduan Lengkap Mengamankan API URL dan API Key pada Aplikasi Web (Standar Industri)

Panduan Lengkap Mengamankan API URL dan API Key pada Aplikasi Web (Standar Industri)

Pada tahun 2023, seorang developer Samsung tidak sengaja mem-push source code ke repositori publik GitHub yang mengandung API Key internal dan kredensial AWS milik perusahaan. Insiden seperti ini terjadi setiap hari, dan korbannya bukan hanya perusahaan besar. Tagihan AWS yang membengkak hingga ratusan juta rupiah dalam semalam akibat API Key yang bocor adalah kisah nyata yang sering dibagikan di forum developer.

Artikel ini membahas secara mendalam dan lengkap dengan contoh kode nyata bagaimana cara mengamankan API URL dan API Key di aplikasi web, mulai dari proyek kecil hingga arsitektur skala produksi.


1. Memahami Ancaman: Apa yang Terjadi Jika API Key Bocor?

Sebelum membahas solusi, penting untuk memahami attack vector yang umum:

  • Source Code Exposure: Key di-commit ke Git repository (bahkan private repo bisa bocor)
  • Frontend Bundling: Variabel REACT_APP_SECRET atau VITE_API_KEY akan tertanam dalam plain text di bundle JavaScript yang bisa dilihat siapapun
  • Log Files: Key muncul di application log, server log, atau error tracking tools
  • Network Interception: Request yang tidak menggunakan HTTPS bisa di-intercept
  • Browser DevTools: Siapapun bisa membuka F12 → Network Tab dan melihat semua request beserta header Authorization
// ❌ KESALAHAN YANG SERING DILAKUKAN — Menyimpan key di .env frontend

// file: .env (di project React/Vue/Angular)
REACT_APP_OPENAI_KEY=sk-proj-abc123...   // BERBAHAYA!
VITE_STRIPE_SECRET=sk_live_xyz789...     // BERBAHAYA!

// Setelah "npm run build", key ini akan muncul di file seperti:
// dist/assets/index-Df8k29xL.js → cari "sk-proj-abc123" → ada!
// Siapapun yang mengunduh halaman web Anda bisa melihatnya.

Enter fullscreen mode Exit fullscreen mode

2. Prinsip Dasar: Defense in Depth

Standar industri modern menggunakan prinsip Defense in Depth — berlapis-lapis keamanan, bukan satu lapisan saja. Jika satu lapisan jebol, lapisan berikutnya masih menahan. Berikut adalah arsitektur berlapis yang akan kita bangun:

  [Browser / Mobile App]
          │  ← hanya boleh tahu URL internal kita
          ▼
  [API Gateway / Reverse Proxy]
          │  ← CORS, Rate Limiting, Auth check
          ▼
  [Backend Server (Node.js / Laravel / etc)]
          │  ← API Key tersimpan di Secret Manager
          ▼
  [Layanan Eksternal]
     (OpenAI, Stripe, dll)

Enter fullscreen mode Exit fullscreen mode

3. Strategi Utama: Backend-for-Frontend (BFF) Pattern

Ini adalah aturan emas nomor satu: Jangan pernah memanggil API pihak ketiga yang membutuhkan secret key langsung dari browser. Selalu gunakan server sebagai perantara.

Implementasi di Node.js (Express)

// backend/routes/ai.js
import express from 'express';
import OpenAI from 'openai';

const router = express.Router();

// API Key HANYA ada di backend, diambil dari environment variable server
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY, // ✅ Aman — hanya ada di server
});

router.post('/generate', async (req, res) => {
  const { prompt } = req.body;

  try {
    const completion = await openai.chat.completions.create({
      model: 'gpt-4o',
      messages: [{ role: 'user', content: prompt }],
    });

    // Frontend hanya menerima hasil, tidak pernah tahu API Key-nya
    res.json({ result: completion.choices[0].message.content });
  } catch (err) {
    res.status(500).json({ error: 'Layanan AI sedang tidak tersedia' });
    // Jangan return error mentah dari library! Bisa mengekspos informasi sensitif
  }
});

export default router;

Enter fullscreen mode Exit fullscreen mode

Di sisi Frontend (React)

// frontend/src/api/ai.js

// ✅ Frontend hanya tahu URL BACKEND KITA SENDIRI, bukan URL OpenAI
const API_BASE = import.meta.env.VITE_API_BASE_URL || 'https://api.aplikasikita.com';

export async function generateText(prompt) {
  const res = await fetch(`${API_BASE}/ai/generate`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${getAccessToken()}`, // token login user
    },
    body: JSON.stringify({ prompt }),
  });

  if (!res.ok) throw new Error('Gagal generate teks');
  return res.json();
}
// ✅ VITE_API_BASE_URL aman untuk diekspos — itu hanyalah URL server kita sendiri
// ❌ Yang TIDAK BOLEH diekspos: OPENAI_API_KEY, DATABASE_URL, dll

Enter fullscreen mode Exit fullscreen mode

4. Menyimpan Secret dengan Aman: Environment Variables & Secret Manager

Menyimpan API Key di file .env di server adalah langkah pertama yang benar, namun ada level yang lebih aman.

Level 1: File .env (Minimum)

# .env di server backend (JANGAN PERNAH commit file ini ke Git!)
OPENAI_API_KEY=sk-proj-...
DATABASE_URL=postgresql://user:pass@localhost/db
STRIPE_SECRET_KEY=sk_live_...

# Pastikan .env ada di .gitignore

Enter fullscreen mode Exit fullscreen mode
# .gitignore — wajib ada
.env
.env.local
.env.production
*.pem
*.key

Enter fullscreen mode Exit fullscreen mode

Level 2: Secret Manager (Standar Industri Production)

Untuk aplikasi production, gunakan layanan Secret Manager khusus. Secret tersimpan terenkripsi, ada audit log siapa yang mengakses kapan, dan bisa di-rotasi tanpa harus merestart server.

// Menggunakan AWS Secrets Manager (Node.js)
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';

const client = new SecretsManagerClient({ region: 'ap-southeast-1' });

async function getSecret(secretName) {
  const command = new GetSecretValueCommand({ SecretId: secretName });
  const response = await client.send(command);
  return JSON.parse(response.SecretString);
}

// Saat aplikasi startup, ambil semua secret sekaligus
const secrets = await getSecret('prod/aplikasi-kita/api-keys');
const openaiClient = new OpenAI({ apiKey: secrets.OPENAI_API_KEY });

// Alternatif gratis: HashiCorp Vault (self-hosted open-source)
// Atau: Doppler, Infisical (SaaS dengan free tier)

Enter fullscreen mode Exit fullscreen mode

Level 3: Rotasi Key Otomatis

Setiap API Key harus memiliki masa hidup (TTL). Jika key bocor, kerugian terbatas pada periode tersebut saja.

// Contoh rotasi key menggunakan cron job (setiap 30 hari)
import cron from 'node-cron';

// Rotasi setiap tanggal 1, pukul 02:00 WIB
cron.schedule('0 2 1 * *', async () => {
  console.log('Memulai rotasi API Key...');

  // 1. Generate key baru dari panel layanan via API (Stripe, dll mendukung ini)
  const newKey = await stripeClient.apiKeys.create({ type: 'restricted' });

  // 2. Update di Secret Manager
  await updateSecret('prod/aplikasi-kita/api-keys', { STRIPE_KEY: newKey.key });

  // 3. Invalidasi key lama
  await stripeClient.apiKeys.del(oldKeyId);

  console.log('Rotasi selesai.');
});

Enter fullscreen mode Exit fullscreen mode

5. Mengamankan API Endpoint dengan Autentikasi & Otorisasi

Setelah API Key aman di backend, endpoint API internal kita pun harus dilindungi agar tidak bisa diakses sembarangan.

Middleware JWT Authentication

// middleware/authenticate.js
import jwt from 'jsonwebtoken';

export function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Token autentikasi tidak ditemukan' });
  }

  const token = authHeader.split(' ')[1];

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    req.user = payload; // { userId, role, email }
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Sesi login telah berakhir, silakan login ulang' });
    }
    return res.status(401).json({ error: 'Token tidak valid' });
  }
}

// Middleware otorisasi berdasarkan role
export function authorize(...roles) {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Anda tidak memiliki akses ke resource ini' });
    }
    next();
  };
}

// Cara pakai di routes:
// router.post('/admin/users', authenticate, authorize('admin'), handler);
// router.post('/ai/generate', authenticate, handler);

Enter fullscreen mode Exit fullscreen mode

6. Konfigurasi CORS yang Benar

CORS bukan alat keamanan utama (tidak bisa mencegah server-to-server request atau curl), tapi penting untuk mencegah website lain "menempelkan" form ke endpoint API kita dari browser pengguna.

// app.js — Konfigurasi CORS production
import cors from 'cors';

const ALLOWED_ORIGINS = [
  'https://aplikasikita.com',
  'https://www.aplikasikita.com',
  // Tambahkan subdomain jika perlu: 'https://app.aplikasikita.com'
];

app.use(cors({
  origin: (origin, callback) => {
    // Izinkan request tanpa origin (Postman, server-to-server) hanya di development
    if (!origin && process.env.NODE_ENV === 'development') {
      return callback(null, true);
    }

    if (ALLOWED_ORIGINS.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error(`Origin ${origin} tidak diizinkan oleh CORS policy`));
    }
  },
  credentials: true,           // Izinkan cookies
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  maxAge: 86400,               // Cache preflight 24 jam (kurangi OPTIONS request)
}));

Enter fullscreen mode Exit fullscreen mode

7. Rate Limiting & Throttling

Rate limiting membatasi frekuensi request agar tidak bisa di-abuse meskipun API key atau session token berhasil dicuri.

// middleware/rateLimiter.js
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import { redisClient } from '../config/redis.js';

// Limit umum untuk semua endpoint
export const generalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 menit
  max: 200,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Terlalu banyak request, coba lagi dalam 15 menit' },
  store: new RedisStore({ client: redisClient }), // Pakai Redis agar limit berlaku di multi-server
});

// Limit ketat untuk endpoint sensitif (login, payment, dll)
export const strictLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 menit
  max: 5,              // 5 request per menit
  message: { error: 'Terlalu banyak percobaan, coba lagi dalam 1 menit' },
  keyGenerator: (req) => req.user?.userId || req.ip, // Limit per user, bukan per IP
  store: new RedisStore({ client: redisClient }),
});

// Cara pakai:
// app.use('/api', generalLimiter);
// app.use('/api/auth/login', strictLimiter);
// app.use('/api/payment', authenticate, strictLimiter);

Enter fullscreen mode Exit fullscreen mode

8. Restriksi API Key di Panel Layanan Eksternal

Ini adalah lapisan damage control — bahkan jika key berhasil dicuri, key tersebut tidak berguna di luar konteks yang sudah kita definisikan.

  Layanan
  Cara Restriksi
  Efek Jika Key Dicuri




  **Google Cloud (Maps, Vision, dll)**
  APIs & Services → Credentials → Edit → Application restrictions: HTTP referrers / IP addresses
  Key hanya bekerja dari domain/IP yang terdaftar


  **Stripe**
  Dashboard → API Keys → Create restricted key → Pilih scope (read/write per resource)
  Key hanya bisa melakukan operasi tertentu (misal: hanya buat payment intent, tidak bisa refund)


  **AWS**
  IAM → Policy → Buat policy spesifik (Least Privilege), tambahkan Condition: aws:SourceIp
  Key hanya bisa akses resource tertentu dari IP server kita


  **OpenAI**
  Platform → API Keys → Create restricted key → Pilih model dan endpoint
  Key hanya bisa pakai model tertentu dengan batas penggunaan
Enter fullscreen mode Exit fullscreen mode

9. Security Headers HTTP yang Wajib Dipasang

// middleware/securityHeaders.js — gunakan library 'helmet' (Node.js)
import helmet from 'helmet';

app.use(helmet({
  // Content Security Policy — mencegah XSS dan data injection
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'nonce-{RANDOM_NONCE}'"],
      connectSrc: ["'self'", 'https://api.aplikasikita.com'],
      imgSrc: ["'self'", 'https://i.ibb.co', 'data:'],
      styleSrc: ["'self'", "'unsafe-inline'"],
    },
  },
  // Mencegah browser "menebak" tipe konten (MIME sniffing)
  noSniff: true,
  // Paksa koneksi HTTPS (HSTS)
  strictTransportSecurity: {
    maxAge: 31536000, // 1 tahun
    includeSubDomains: true,
    preload: true,
  },
  // Mencegah clickjacking
  frameguard: { action: 'deny' },
  // Sembunyikan informasi framework dari header
  hidePoweredBy: true,
}));

Enter fullscreen mode Exit fullscreen mode

10. Checklist Audit Keamanan API

Gunakan checklist berikut sebelum deploy ke production:

  Item
Status
Prioritas

Tidak ada API Key/Secret di source code frontend☐Kritis
File .env tidak di-commit ke repository (ada di .gitignore)☐Kritis
Semua endpoint sensitif dilindungi autentikasi (JWT/Session)☐Kritis
HTTPS aktif (bukan HTTP) di semua environment production☐Kritis
Rate Limiting aktif di endpoint login, payment, dan API publik☐Tinggi
CORS dikonfigurasi hanya untuk domain yang diizinkan☐Tinggi
API Key eksternal diberi restriksi IP/Referrer di panel layanan☐Tinggi
Secret tersimpan di Secret Manager (bukan hanya file .env)☐Menengah
Security Headers (Helmet/CSP/HSTS) terpasang☐Menengah
Rotasi API Key dijadwalkan secara berkala☐Menengah
Monitoring & alerting untuk request anomali aktif☐Menengah
Scanning secret di repository menggunakan tool (GitLeaks, TruffleHog)☐Baik untuk dimiliki

Enter fullscreen mode Exit fullscreen mode




Kesimpulan

Mengamankan API bukan satu langkah, melainkan sebuah sistem berlapis. Mulai dari yang paling fundamental: jangan pernah expose secret key ke frontend. Gunakan pola BFF/Proxy, autentikasi semua endpoint, konfigurasi CORS dengan ketat, aktifkan rate limiting, dan manfaatkan Secret Manager untuk penyimpanan key yang aman di production. Dengan menerapkan semua lapisan ini, bahkan jika satu lapisan berhasil ditembus, lapisan selanjutnya masih melindungi aplikasi dan pengguna Anda.


Artikel ini pertama kali diterbitkan di SavefileArchive.

Top comments (0)