Rate limiting pada endpoint login, OTP, dan reset password adalah kontrol dasar untuk menahan brute force, credential stuffing, dan penyalahgunaan API yang memicu SMS/email OTP berlebihan. Di Go Fiber, pendekatan yang aman biasanya bukan sekadar membatasi per IP, tetapi menggabungkan per IP, per user identifier, dan kadang per kombinasi keduanya agar serangan lebih mahal tanpa terlalu banyak memblokir pengguna sah.
Artikel ini membahas implementasi praktis di Go Fiber: desain key yang aman, middleware, respons HTTP 429 dengan Retry-After, logging, metrik, serta kapan in-memory limiter cukup dan kapan Anda perlu Redis untuk state terdistribusi. Fokusnya adalah endpoint sensitif seperti /login, /otp/request, /otp/verify, dan /password/reset.
Mengapa endpoint login, OTP, dan reset password perlu limit yang berbeda
Tidak semua endpoint sensitif punya pola abuse yang sama. Karena itu, limit yang dipasang sebaiknya menyesuaikan risiko dan biaya operasionalnya.
- Login: target utama brute force dan credential stuffing. Penyerang bisa mencoba banyak password untuk satu akun, atau satu password untuk banyak akun.
- OTP request: sering disalahgunakan untuk membanjiri SMS/email, menaikkan biaya, dan mengganggu pengguna.
- OTP verify: rentan brute force kode OTP jika panjang kode kecil dan masa berlaku cukup lama.
- Reset password: bisa dipakai untuk enumerasi akun, spam email, atau denial-of-service ringan ke pengguna tertentu.
Karena pola abuse berbeda, limit juga sebaiknya berbeda. Misalnya:
- Login: batasi per IP dan per identifier akun.
- OTP request: batasi ketat per identifier dan per destination channel.
- OTP verify: batasi per session/challenge ID dan per identifier.
- Reset password: batasi per identifier dan per IP, serta samakan respons agar tidak membocorkan apakah akun ada.
Memilih dimensi limit: per IP, per user identifier, atau kombinasi
1. Limit per IP
Limit per IP adalah lapisan pertama yang murah dan mudah. Ini efektif untuk menahan serangan volumetrik dari satu sumber jaringan.
Kelebihan:
- Mudah diterapkan.
- Baik untuk mengurangi flood dari bot sederhana.
- Tidak bergantung pada isi request.
Kekurangan:
- Banyak pengguna sah bisa berbagi IP yang sama karena NAT, kantor, kampus, carrier mobile, atau proxy perusahaan.
- Penyerang bisa menyebar request dari banyak IP.
- Kalau salah baca header proxy, IP bisa salah atau bahkan bisa di-spoof.
2. Limit per user identifier
User identifier bisa berupa email, nomor telepon, username, atau ID akun yang dikirim klien. Untuk login, identifier biasanya username/email. Untuk OTP, bisa berupa nomor telepon atau email tujuan.
Kelebihan:
- Efektif melindungi akun target tertentu dari brute force.
- Lebih relevan untuk abuse OTP dan reset password.
Kekurangan:
- Jika dipakai sendiri, penyerang masih bisa mencoba banyak identifier dari satu IP.
- Perlu normalisasi input yang konsisten agar key tidak mudah diakali.
- Harus berhati-hati agar logging atau key storage tidak membocorkan data sensitif.
3. Kombinasi per IP + per identifier
Untuk endpoint sensitif, ini biasanya pilihan terbaik. Anda bisa menerapkan beberapa lapisan sekaligus:
- Global per IP untuk menahan flood kasar.
- Per identifier untuk melindungi akun atau nomor tertentu.
- Per IP + identifier untuk mempersempit percobaan berulang dari satu asal ke satu target.
Contoh kebijakan yang lebih realistis:
POST /login: limit per IP dan per email/username.POST /otp/request: limit per IP, per nomor/email tujuan, dan cooldown per target.POST /otp/verify: limit per challenge ID atau identifier, plus lock sementara setelah beberapa kegagalan.POST /password/reset: limit per IP dan per identifier, dengan respons generik agar tidak ada enumerasi akun.
Jangan bergantung pada satu dimensi limit. Limit per IP saja sering terlalu kasar, sedangkan limit per identifier saja mudah disebar dari banyak IP.
Trade-off algoritma: fixed window, sliding window, dan token bucket
Fixed window
Paling sederhana: hitung jumlah request dalam jendela waktu tetap, misalnya 5 request per 10 menit.
Kelebihan:
- Mudah diimplementasikan.
- Murah secara komputasi dan penyimpanan.
Kekurangan:
- Ada efek boundary burst: penyerang bisa mengirim request di akhir jendela lalu langsung lagi di awal jendela berikutnya.
Kapan cocok: endpoint internal sederhana atau sistem dengan toleransi burst kecil.
Sliding window
Sliding window menghitung aktivitas dalam rentang waktu berjalan, sehingga lebih adil dibanding fixed window.
Kelebihan:
- Lebih akurat dan halus.
- Mengurangi efek burst di perbatasan window.
Kekurangan:
- Implementasi lebih kompleks.
- Biasanya butuh state atau operasi storage yang lebih mahal.
Kapan cocok: endpoint sensitif seperti login dan OTP verify jika Anda ingin kontrol yang lebih presisi, terutama di sistem terdistribusi dengan Redis.
Token bucket
Token bucket memberi kapasitas burst terbatas tetapi mengisi ulang token secara bertahap. Request boleh lewat jika token tersedia.
Kelebihan:
- Baik untuk traffic nyata yang sesekali burst.
- Fleksibel untuk mengatur steady rate dan burst capacity.
Kekurangan:
- Untuk kasus anti-brute-force yang sangat ketat, perlu desain parameter hati-hati.
- Lebih sulit dipahami dibanding fixed window.
Kapan cocok: API publik yang perlu toleran terhadap burst kecil, tetapi tetap membatasi laju rata-rata.
Pilih yang mana untuk login dan OTP?
- Login: fixed window cukup jika sederhana, tetapi sliding window atau token bucket lebih baik jika ingin mengurangi burst abuse.
- OTP request: fixed window + cooldown sering cukup, misalnya pembatasan per target dan jarak minimum antar request.
- OTP verify: sliding window atau fixed window ketat cocok karena endpoint ini sensitif terhadap brute force kode.
Yang paling penting bukan nama algoritmanya, tetapi apakah kebijakannya relevan terhadap pola serangan dan tidak terlalu menyiksa pengguna sah.
Desain key rate limiting yang aman
Key rate limit harus stabil, aman, dan tidak memudahkan penyerang menghindari limit. Hindari menyimpan data mentah jika tidak perlu.
Prinsip desain key
- Normalisasi identifier: email di-lowercase dan di-trim; nomor telepon diubah ke format yang konsisten.
- Hash identifier: jangan simpan email/telepon mentah sebagai key Redis atau log jika tidak perlu. Gunakan HMAC atau hash dengan secret server agar tidak mudah ditebak.
- Namespace per endpoint: bedakan key login, OTP request, OTP verify, dan reset password.
- Sertakan versi skema: memudahkan migrasi kebijakan ke depan.
Contoh bentuk key:
rl:v1:login:ip:{client_ip}rl:v1:login:id:{hmac_identifier}rl:v1:login:pair:{client_ip}:{hmac_identifier}rl:v1:otp_request:dst:{hmac_destination}rl:v1:otp_verify:challenge:{challenge_id}
Jika Anda menyimpan identifier mentah di key, siapa pun yang punya akses observability atau storage bisa melihat data sensitif. HMAC dengan secret aplikasi lebih aman karena hasilnya konsisten untuk pencocokan, tetapi tidak mudah dibalik.
Implementasi praktis di Go Fiber
Di Go Fiber, Anda bisa menerapkan rate limiting sebagai middleware khusus untuk endpoint sensitif. Contoh di bawah memakai store in-memory untuk menunjukkan struktur. Untuk sistem multi-instance, pola yang sama sebaiknya dipindahkan ke Redis atau storage terpusat.
Contoh middleware rate limiting sederhana
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strings"
"sync"
"time"
"github.com/gofiber/fiber/v2"
)
type counter struct {
Count int
ResetTime time.Time
}
type memoryLimiter struct {
mu sync.Mutex
items map[string]counter
}
func newMemoryLimiter() *memoryLimiter {
return &memoryLimiter{items: make(map[string]counter)}
}
func (l *memoryLimiter) Allow(key string, limit int, window time.Duration) (bool, time.Time) {
now := time.Now()
l.mu.Lock()
defer l.mu.Unlock()
item, ok := l.items[key]
if !ok || now.After(item.ResetTime) {
l.items[key] = counter{Count: 1, ResetTime: now.Add(window)}
return true, now.Add(window)
}
if item.Count >= limit {
return false, item.ResetTime
}
item.Count++
l.items[key] = item
return true, item.ResetTime
}
func normalizeIdentifier(v string) string {
return strings.ToLower(strings.TrimSpace(v))
}
func hmacIdentifier(secret, value string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(value))
return hex.EncodeToString(mac.Sum(nil))
}
func clientIP(c *fiber.Ctx) string {
return c.IP()
}
func LoginRateLimitMiddleware(l *memoryLimiter, secret string) fiber.Handler {
return func(c *fiber.Ctx) error {
type loginRequest struct {
Email string `json:"email"`
Username string `json:"username"`
}
var req loginRequest
_ = c.BodyParser(&req)
identifier := req.Email
if identifier == "" {
identifier = req.Username
}
identifier = normalizeIdentifier(identifier)
ip := clientIP(c)
idHash := "anonymous"
if identifier != "" {
idHash = hmacIdentifier(secret, identifier)
}
allowedIP, resetIP := l.Allow("rl:v1:login:ip:"+ip, 20, 10*time.Minute)
allowedID, resetID := l.Allow("rl:v1:login:id:"+idHash, 5, 10*time.Minute)
allowedPair, resetPair := l.Allow("rl:v1:login:pair:"+ip+":"+idHash, 5, 10*time.Minute)
if !allowedIP || !allowedID || !allowedPair {
resetAt := resetIP
if resetID.Before(resetAt) == false {
resetAt = resetID
}
if resetPair.Before(resetAt) == false {
resetAt = resetPair
}
retryAfter := int(time.Until(resetAt).Seconds())
if retryAfter < 1 {
retryAfter = 1
}
c.Set("Retry-After", strconv.Itoa(retryAfter))
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
"error": "too_many_requests",
"message": "Terlalu banyak percobaan login. Coba lagi nanti.",
})
}
return c.Next()
}
}Contoh di atas menunjukkan beberapa hal penting:
- Satu endpoint dapat memiliki lebih dari satu limiter.
- Identifier dinormalisasi lalu di-HMAC sebelum dipakai sebagai key.
- Jika limit terlampaui, server mengembalikan HTTP 429 dan header Retry-After.
Namun, contoh ini juga punya keterbatasan:
- In-memory per instance: tidak sinkron antar pod/instance.
- Fixed window sederhana: cukup untuk demo, belum ideal untuk beban besar.
- BodyParser di middleware: perlu hati-hati agar tidak mengganggu alur parsing utama. Dalam implementasi nyata, Anda bisa mengekstrak field seperlunya atau menggunakan middleware yang disusun khusus.
Catatan penting pada contoh kode
Pada implementasi produksi, Anda sebaiknya memperbaiki beberapa detail:
- Pastikan paket yang dibutuhkan seperti
strconvdiimpor jika contoh dipakai langsung. - Gunakan store terpusat untuk deployment multi-instance.
- Tambahkan pembersihan key kadaluarsa jika memakai map in-memory jangka panjang.
- Jangan menjadikan body parsing berat sebagai bottleneck pada endpoint ramai.
Memasang middleware pada route sensitif
app := fiber.New()
limiter := newMemoryLimiter()
secret := "server-side-secret"
app.Post("/login", LoginRateLimitMiddleware(limiter, secret), loginHandler)
app.Post("/otp/request", otpRequestRateLimitMiddleware(limiter, secret), otpRequestHandler)
app.Post("/otp/verify", otpVerifyRateLimitMiddleware(limiter, secret), otpVerifyHandler)
app.Post("/password/reset", resetPasswordRateLimitMiddleware(limiter, secret), resetPasswordHandler)Jangan pakai angka limit yang sama untuk semua endpoint. OTP request biasanya lebih ketat karena ada biaya nyata dan potensi gangguan ke pengguna.
Respons HTTP 429 yang benar
Ketika limit terlampaui, gunakan 429 Too Many Requests. Selain status code, berikan informasi yang membantu klien tanpa membocorkan terlalu banyak detail.
Header dan body yang disarankan
- Retry-After: jumlah detik atau waktu absolut kapan klien boleh mencoba lagi.
- Body JSON ringkas: kode error dan pesan generik.
{
"error": "too_many_requests",
"message": "Terlalu banyak permintaan. Coba lagi nanti."
}Untuk endpoint autentikasi, hindari body yang terlalu spesifik seperti “email ini diblokir” atau “IP Anda diblokir selama 8 menit” jika informasi itu bisa membantu penyerang memetakan kebijakan Anda. Pesan yang terlalu detail juga bisa memudahkan enumerasi atau tuning serangan.
Kapan in-memory cukup, kapan perlu Redis
In-memory cukup jika
- Aplikasi hanya berjalan di satu instance.
- Skala masih kecil.
- Anda menerima bahwa restart aplikasi akan mereset counter.
- Tujuan utama adalah perlindungan dasar, bukan konsistensi kuat antar node.
Perlu Redis jika
- Aplikasi berjalan di banyak instance/pod.
- Request bisa masuk ke node yang berbeda di belakang load balancer.
- Anda perlu state rate limit yang konsisten.
- Anda ingin menghindari bypass karena penyerang menyebar request ke banyak instance.
Pada deployment multi-instance, limiter in-memory menyebabkan race of enforcement: satu pengguna bisa lolos lebih banyak percobaan karena tiap instance punya counter sendiri. Ini sering terlihat saat traffic dibagi round-robin oleh load balancer.
Apa yang perlu diperhatikan saat memakai Redis
- Gunakan operasi atomik agar increment dan expiry konsisten.
- Pastikan TTL diatur dengan benar supaya key tidak hidup terus.
- Untuk sliding window atau token bucket, gunakan desain yang memang aman terhadap konkurensi.
- Perhatikan latensi tambahan ke Redis, terutama jika rate limiter dipasang di setiap request publik.
Jika implementasi Redis Anda tidak atomik, dua request bersamaan bisa membaca nilai lama lalu sama-sama dianggap lolos. Itu sebabnya rate limiter terdistribusi sebaiknya dibangun di atas operasi yang menjamin konsistensi per key.
Kebijakan praktis untuk login, OTP, dan reset password
Login
- Batasi per IP untuk flood umum.
- Batasi per identifier untuk melindungi akun target.
- Batasi per pasangan IP+identifier untuk percobaan berulang dari satu sumber.
- Pertimbangkan jeda bertahap atau captcha setelah pola mencurigakan, bukan blok permanen.
OTP request
- Batasi ketat per destination seperti nomor telepon atau email.
- Tambahkan cooldown minimum antar permintaan, misalnya agar pengguna tidak bisa meminta kode berulang dalam hitungan detik.
- Batasi juga per IP untuk menahan abuse massal.
- Jika ada banyak channel, bedakan limit SMS dan email sesuai biaya dan risiko.
OTP verify
- Batasi jumlah percobaan per challenge atau per identifier.
- Setelah beberapa kegagalan, invalidasi challenge dan minta OTP baru.
- Jangan beri tahu apakah kode hampir benar atau salah digit tertentu.
Reset password
- Batasi per IP dan per identifier.
- Respons harus generik, misalnya: “Jika akun terdaftar, instruksi akan dikirim.”
- Jangan membedakan waktu respons terlalu jauh antara akun ada dan tidak ada jika memungkinkan.
Logging dan metrik yang wajib ada
Rate limiter tanpa observability sulit dioperasikan. Anda perlu tahu apakah limiter benar-benar menahan serangan atau justru mengganggu trafik normal.
Data log yang berguna
- Nama endpoint.
- Dimensi limiter yang kena: IP, identifier, atau pair.
- IP klien yang sudah dinormalisasi sesuai kebijakan proxy.
- Hash identifier, bukan identifier mentah.
- Status allowed/blocked.
- Retry-After atau waktu reset.
- Request ID atau trace ID untuk korelasi.
Hindari menyimpan password, OTP, token reset, atau data PII mentah di log.
Metrik penting
- Total request per endpoint.
- Jumlah 429 per endpoint.
- Block rate per dimensi limiter.
- Top targeted identifiers dalam bentuk hash atau agregat anonim.
- Latency tambahan dari limiter/store.
- Error rate ke Redis/store.
Jika jumlah 429 tinggi tetapi login sukses juga tinggi dari IP yang sama, kebijakan mungkin terlalu longgar. Jika 429 tinggi dan complaint pengguna naik dari jaringan kantor atau operator seluler, limit per IP mungkin terlalu ketat.
Jebakan umum yang sering membuat rate limiting gagal
1. Memblokir pengguna sah di balik NAT atau proxy bersama
Ini masalah klasik pada limit per IP. Satu kantor, kampus, atau operator seluler bisa membuat banyak pengguna tampil dari IP publik yang sama. Jika limit per IP terlalu ketat, login pengguna sah ikut gagal.
Cara mengurangi risiko:
- Jangan hanya mengandalkan per IP.
- Gunakan kombinasi per IP dan per identifier.
- Atur limit per IP lebih longgar dibanding per identifier.
- Pantau distribusi 429 dari IP yang mewakili jaringan bersama.
2. Salah percaya header IP dari klien
Jika aplikasi berada di belakang reverse proxy atau load balancer, IP klien asli sering diteruskan lewat header seperti X-Forwarded-For. Masalahnya, header ini bisa di-spoof jika aplikasi langsung percaya input dari internet tanpa boundary yang jelas.
Prinsip aman:
- Percaya header forward hanya jika request benar-benar datang dari proxy tepercaya.
- Konfigurasikan chain proxy dengan benar.
- Jangan ambil nilai header mentah begitu saja untuk rate limit atau audit.
Kalau sumber IP salah, seluruh kebijakan rate limit ikut salah: pengguna sah bisa diblokir, sementara penyerang bisa lolos dengan memalsukan header.
3. Race condition pada multi-instance
Pada deployment dengan banyak instance, limiter in-memory menyebabkan limit tidak konsisten. Penyerang bisa mendapat kuota efektif berlipat karena counter terpisah per node.
Solusi:
- Gunakan Redis atau store terpusat.
- Pastikan increment dan expiry dilakukan atomik.
- Uji dengan traffic paralel, bukan hanya request serial.
4. Key terlalu mudah diakali
Jika email tidak dinormalisasi, [email protected] dan [email protected] bisa dianggap berbeda. Jika nomor telepon punya banyak format, penyerang bisa melewati limit dengan variasi representasi yang sebenarnya sama.
Solusi: normalisasi input sebelum membentuk key dan pakai satu aturan yang konsisten di seluruh endpoint.
5. Mengungkap terlalu banyak informasi lewat respons
Respons yang terlalu detail membantu penyerang menyesuaikan pola serangan. Untuk endpoint sensitif, cukup berikan 429 dan Retry-After tanpa menjelaskan detail internal kebijakan.
Strategi pengujian dan debugging
Uji skenario yang benar-benar relevan
- Kirim request berulang dari satu IP ke satu akun.
- Kirim request dari satu IP ke banyak akun.
- Kirim request dari banyak IP ke satu akun.
- Uji dari environment yang meniru proxy/load balancer produksi.
- Uji paralel untuk melihat apakah ada race condition.
Hal yang perlu dicek saat debugging
- Apakah IP klien yang terbaca benar?
- Apakah identifier sudah dinormalisasi konsisten?
- Apakah TTL/reset time sesuai ekspektasi?
- Apakah semua instance memakai store yang sama?
- Apakah 429 muncul pada endpoint dan dimensi yang tepat?
Jika pengguna mengeluh tidak bisa login padahal jarang mencoba, periksa apakah mereka berada di balik NAT besar atau apakah proxy forwarding salah konfigurasi.
Rekomendasi implementasi yang realistis
Untuk sebagian besar aplikasi Go Fiber, pendekatan yang masuk akal adalah:
- Pakai kombinasi limit per IP dan per identifier untuk login, OTP, dan reset password.
- Gunakan fixed window sederhana jika aplikasi masih satu instance dan kebutuhan operasional belum kompleks.
- Naik ke Redis saat aplikasi sudah multi-instance atau butuh enforcement yang konsisten.
- Hash/HMAC identifier sebelum disimpan sebagai key atau log field.
- Kembalikan HTTP 429 dengan Retry-After.
- Tambahkan logging dan metrik sejak awal, karena tuning limiter tanpa observability hampir selalu berujung trial-and-error.
Jika harus memilih satu pelajaran utama: rate limiting untuk autentikasi bukan hanya soal membatasi request, tetapi soal memilih identitas yang dibatasi, algoritma yang sesuai, dan store yang konsisten dengan arsitektur deployment Anda.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!