Verifikasi JSON Web Token (JWT) berbasis asymmetric key (seperti RS256 atau ES256) membutuhkan public key dari Identity Provider (IdP) melalui endpoint JSON Web Key Set (JWKS). Mengambil JWKS via HTTP call pada setiap request API memicu latensi tinggi dan pemborosan bandwidth. Sebaliknya, caching JWKS tanpa strategi refresh dinamis menyebabkan autentikasi gagal total saat IdP melakukan rotasi kunci kriptografi (key rotation).
Dilema JWKS: Per-Request Latency vs Cache Stale
Dua pendekatan ekstrem yang sering menjadi jebakan arsitektural pada API gateway atau microservice Go:
- Fetch Per-Request: Setiap request validasi JWT memicu HTTP
GETke endpoint/.well-known/jwks.jsonmilik IdP (Auth0, Keycloak, Okta, Firebase). Latensi auth bertambah 50–300ms per request. Jika IdP mengalami rate limiting atau degradasi jaringan, seluruh API ikut lumpuh. - Static/Naive In-Memory Caching: JWKS di-fetch sekali saat aplikasi start atau di-cache dengan TTL panjang tanpa mekanisme invalidasi. Ketika IdP memutar asymmetric key (terjadwal maupun darurat akibat kebocoran private key), token baru dikirim dengan Header Key ID (
kid) baru. Server menolak token tersebut (401 Unauthorized) karenakidbaru belum terdaftar di cache.
Aturan Kunci: Cache JWKS harus bersifat dinamis. Cache disimpan di memory dengan background refresh, namun wajib menyediakan fallback on-demand refresh ketika menerima
kidyang belum dikenali.
Arsitektur Solusi: Hybrid In-Memory Cache
Solusi yang tangguh menggabungkan tiga pilar arsitektur:
- In-Memory Cache dengan TTL: Public keys disimpan di memori aplikasi untuk validasi sub-milidetik tanpa round-trip I/O.
- On-Demand Refresh saat KID Hilang: Jika incoming JWT memiliki
kidyang tidak ada di cache, sistem mengasumsikan rotasi kunci telah terjadi dan memicu fetch JWKS ulang ke IdP secara sinkron sebelum memvalidasi token. - Rate-Limited Refresh: Attacker dapat mengeksploitasi on-demand refresh dengan mengirim ribuan request berisi random
kid(Cache Snooping / Denial of Service terhadap IdP). Rate limiter membatasi frekuensi refresh JWKS maksimum (misal: 1 kali per 1–5 menit jika ada miss berturut-turut).
Implementasi Middleware Go Fiber
Gunakan pustaka standar github.com/MicahParks/keyfunc/v2 bersama github.com/golang-jwt/jwt/v5 di framework Go Fiber.
package middleware
import (
"errors"
"strings"
"time"
"github.com/MicahParks/keyfunc/v2"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
type AuthConfig struct {
JWKSURL string
RateLimit time.Duration
FetchTimeout time.Duration
}
func NewJWTMiddleware(cfg AuthConfig) (fiber.Handler, error) {
// Konfigurasi keyfunc dengan rate limiting dan on-demand refresh
options := keyfunc.Options{
RefreshRateLimit: cfg.RateLimit,
RefreshTimeout: cfg.FetchTimeout,
RefreshUnknownKID: true, // Otomatis fetch jika kid tidak ada di cache
}
jwks, err := keyfunc.Get(cfg.JWKSURL, options)
if err != nil {
return nil, err
}
return func(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "missing or malformed authorization token",
})
}
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
// Validasi token dan signature menggunakan cached keyfunc
token, err := jwt.Parse(tokenStr, jwks.Keyfunc)
if err != nil {
// Tangani kegagalan IdP unreachable saat refresh on-demand
if errors.Is(err, keyfunc.ErrKIDNotFound) {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "invalid key identifier (kid)",
})
}
// Bedakan error sistem (503) dengan error client invalid/expired (401)
if errors.Is(err, jwt.ErrTokenExpired) {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "token has expired",
})
}
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "token verification failed: " + err.Error(),
})
}
if !token.Valid {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "invalid token claims",
})
}
// Simpan claims ke Locals untuk digunakan handler berikutnya
c.Locals("user", token.Claims)
return c.Next()
}, nil
}
Inisialisasi pada Server Fiber
package main
import (
"log"
"time"
"github.com/gofiber/fiber/v2"
"yourproject/middleware"
)
func main() {
app := fiber.New()
jwtAuth, err := middleware.NewJWTMiddleware(middleware.AuthConfig{
JWKSURL: "https://idp.example.com/.well-known/jwks.json",
RateLimit: 1 * time.Minute,
FetchTimeout: 5 * time.Second,
})
if err != nil {
log.Fatalf("Gagal inisialisasi JWKS client: %v", err)
}
api := app.Group("/api", jwtAuth)
api.Get("/profile", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"status": "authenticated"})
})
log.Fatal(app.Listen(":3000"))
}
Strategi Error Handling: 401 Unauthorized vs 503 Service Unavailable
Bedakan respon error berdasarkan akar penyebab kegagalan parsing:
- HTTP 401 Unauthorized: Token cacat, expired (
jwt.ErrTokenExpired), algoritma tidak cocok (misalnoneatauHS256saat IdP menetapkanRS256), ataukidtetap tidak ditemukan setelah proses refresh selesai dilakukan. - HTTP 503 Service Unavailable: Request ke IdP timeout saat mengeksekusi on-demand refresh, DNS lookup IdP gagal, atau IdP mengembalikan HTTP 5xx. Jangan kirim 401 saat IdP down; klien API butuh indikasi bahwa infrastruktur upstream sedang terganggu agar dapat mengaktifkan mekanisme retry/backoff.
Uji Kasus: Simulasi Rotasi Kunci (Key Rotation)
Lakukan verifikasi ketahanan middleware terhadap rotasi kunci dengan tahapan skenario berikut:
- Baseline Validasi: Terbitkan token A dengan
kid: "key-2025-a". Kirim ke API. Token valid seketika via in-memory cache tanpa HTTP fetch tambahan. - Rotasi di IdP: Tambahkan public key baru pada JWKS IdP dengan
kid: "key-2025-b". - Request Token Baru: Kirim request menggunakan token B (
kid: "key-2025-b"). Keyfunc mendeteksi unknown key, memicu HTTPGETke IdP, memperbarui storage cache, dan memvalidasi token sukses tanpa perlu restart aplikasi. - Mitigasi DoS via Random KID: Kirim 50 request konkuren dengan token acak (
kid: "fake-1",kid: "fake-2", ...). Request pertama memicu refresh IdP; 49 request berikutnya tertahan olehRefreshRateLimitdan langsung ditolak 401 tanpa membanjiri IdP.
Trade-offs & Praktik Terbaik
- Worker Thread Safety: Pustaka
keyfuncmengelola sinkronisasisync.RWMutexsecara internal saat read/write cache. Hindari membuat map cache manual di luar pustaka teruji untuk mencegah race condition. - Cold Start Latency: Inisialisasi
keyfunc.Getmemblokir hingga fetch pertama berhasil. Pastikan dependensi jaringan IdP siap sebelum memanggilapp.Listenpada Go Fiber.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!