Episode ini membangun autentikasi di Gin: BasicAuth middleware, JWT dengan golang-jwt, alur access token dan refresh token, session cookie, serta penyimpanan dan revocation token di Redis.

Setelah aplikasi bisa mengelola data, pertanyaan berikutnya: siapa pengguna ini dan apakah ia memang dia? Episode 13 ini membedah autentikasi di Gin — memverifikasi identitas pengguna. Kalian akan belajar BasicAuth untuk kasus sederhana, JWT dengan golang-jwt untuk API token-based, alur access token dan refresh token yang sehat, session cookie untuk aplikasi web, serta penyimpanan dan revocation token di Redis.
Autentikasi bukan tempat untuk menemukan ulang roda. Pola yang kalian pelajari di sini adalah pola industri yang sudah teruji: token berumur pendek untuk akses, token berumur panjang yang bisa dicabut untuk refresh. Episode 14 akan melanjutkan ke otorisasi (siapa boleh melakukan apa).
Gin menyediakan middleware BasicAuth untuk kredensial sederhana:
accounts := gin.Accounts{
"arman": "rahasia123",
}
r.GET("/admin", gin.BasicAuth(accounts), func(c *gin.Context) {
user := c.MustGet(gin.AuthUserKey).(string)
c.JSON(200, gin.H{"user": user})
})gin.BasicAuth(accounts) menolak request tanpa header Authorization yang valid. Username tervalidasi bisa dibaca lewat c.MustGet(gin.AuthUserKey). BasicAuth cocok untuk tooling internal, health check, atau tahap prototyping — bukan untuk aplikasi publik, karena kredensial bisa dengan mudah di-extract dari header.
Untuk API berbasis token, JWT adalah standar de facto. Install dan tanda tangani token HS256:
go get github.com/golang-jwt/jwt/v5func generateAccessToken(userID int64) (string, error) {
claims := jwt.MapClaims{
"sub": userID,
"iat": time.Now().Unix(),
"exp": time.Now().Add(15 * time.Minute).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(secret)
}jwt.NewWithClaims(jwt.SigningMethodHS256, claims) membangun token dengan klaim sub (subject/user id) dan exp (expiry). SignedString(secret) menandatanganinya dengan secret rahasia. Simpan secret di environment, bukan di kode — kembali ke praktik episode 10.
Bungkus verifikasi dalam middleware yang menyimpan identitas di context:
func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
raw := c.GetHeader("Authorization")
if !strings.HasPrefix(raw, "Bearer ") {
c.AbortWithStatusJSON(401, gin.H{"error": "token wajib"})
return
}
token, err := parseToken(strings.TrimPrefix(raw, "Bearer "))
if err != nil || !token.Valid {
c.AbortWithStatusJSON(401, gin.H{"error": "token tidak valid"})
return
}
claims := token.Claims.(jwt.MapClaims)
c.Set("user_id", int64(claims["sub"].(float64)))
c.Next()
}
}c.GetHeader("Authorization") membaca token dari header. Setelah valid, c.Set("user_id", ...) menyimpan identitas yang bisa dibaca handler berikutnya dengan c.Get("user_id") — persis pola yang dipakai middleware di episode 6.
Access token berumur pendek mengurangi risiko jika bocor. Refresh token berumur panjang dipakai hanya untuk mendapatkan access token baru:
r.POST("/login", loginHandler)
r.POST("/refresh", refreshHandler)
func refreshHandler(c *gin.Context) {
var req struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// validasi refresh token, lalu:
access, err := generateAccessToken(userID)
if err != nil {
c.JSON(500, gin.H{"error": "gagal membuat token"})
return
}
c.JSON(200, gin.H{"access_token": access})
}Alur refreshHandler memverifikasi refresh token yang dikirim klien, memastikan masih terdaftar, lalu menerbitkan access token baru. Klien memakai access token untuk request API dan menyimpan refresh token di tempat yang lebih aman.
Untuk aplikasi web klasik, session bisa disimpan dalam cookie yang terverifikasi:
func loginHandler(c *gin.Context) {
// verifikasi kredensial...
sessionID := newSessionID()
c.SetCookie("session", sessionID, 3600, "/", "", true, true)
c.JSON(200, gin.H{"message": "login berhasil"})
}
func profileHandler(c *gin.Context) {
sessionID, err := c.Cookie("session")
if err != nil {
c.JSON(401, gin.H{"error": "belum login"})
return
}
userID, ok := sessionStore.Get(sessionID)
if !ok {
c.JSON(401, gin.H{"error": "session kadaluwarsa"})
return
}
c.JSON(200, gin.H{"user_id": userID})
}c.SetCookie("session", sessionID, 3600, "/", "", true, true) menulis cookie dengan flag Secure (hanya lewat HTTPS) dan HttpOnly (tidak bisa dibaca JavaScript). Nilai cookie hanyalah pengenal; data session disimpan di server (misalnya Redis), bukan di cookie — ini mencegah tamper.
Refresh token harus bisa dicabut. Simpan di Redis dengan TTL sama dengan umur token:
go get github.com/redis/go-redis/v9ctx := context.Background()
key := fmt.Sprintf("refresh:%d", userID)
rdb.Set(ctx, key, tokenHash, 7*24*time.Hour)rdb.Set(ctx, key, tokenHash, 7*24*time.Hour) menyimpan hash refresh token selama 7 hari. Saat refresh, bandingkan token yang datang dengan nilai di Redis; jika cocok, terbitkan access token baru. Logout cukup menghapus key — token langsung tidak berlaku.
Inti yang harus dibawa pulang:
gin.BasicAuth untuk kasus sederhana, bukan untuk aplikasi publik.golang-jwt memakai sub dan exp, verifikasi algoritma tanda tangan.c.Set.Secure dan HttpOnly.Del.Di episode 14 selanjutnya kita akan membedah RBAC, OAuth2 & security middleware — middleware permission dan RBAC, integrasi OAuth2 dan OpenID Connect dengan Keycloak, security headers, CSRF untuk form, serta rate limiting dengan golang.org/x/time/rate.