This episode builds authentication in Gin: the BasicAuth middleware, JWT with golang-jwt, the access token and refresh token flow, session cookies, and token storage and revocation in Redis.

Once your application can manage data, the next question is: who is this user, and are they really who they say they are? This episode 13 dissects authentication in Gin — verifying user identity. You'll learn BasicAuth for simple cases, JWT with golang-jwt for token-based APIs, a healthy access token and refresh token flow, session cookies for web applications, and token storage and revocation in Redis.
Authentication isn't a place to reinvent the wheel. The patterns you learn here are battle-tested industry patterns: short-lived tokens for access, long-lived revocable tokens for refresh. Episode 14 will continue into authorization (who may do what).
Gin provides the BasicAuth middleware for simple credentials:
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) rejects requests without a valid Authorization header. The validated username can be read via c.MustGet(gin.AuthUserKey). BasicAuth suits internal tooling, health checks, or prototyping — not public applications, because credentials can easily be extracted from the header.
For token-based APIs, JWT is the de facto standard. Install it and sign a token with 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) builds a token with the sub (subject/user id) and exp (expiry) claims. SignedString(secret) signs it with a secret key. Store the secret in the environment, not in code — going back to the practice from episode 10.
Wrap verification in middleware that stores the identity in the 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") reads the token from the header. Once valid, c.Set("user_id", ...) stores the identity that the next handler can read with c.Get("user_id") — exactly the pattern middleware used in episode 6.
A short-lived access token reduces risk if leaked. A long-lived refresh token is used only to obtain a new access token:
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})
}The refreshHandler flow verifies the refresh token the client sent, confirms it's still registered, then issues a new access token. Clients use the access token for API requests and store the refresh token somewhere more secure.
For classic web applications, the session can be stored in a verified cookie:
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) writes a cookie with the Secure flag (HTTPS only) and HttpOnly (not readable by JavaScript). The cookie value is only an identifier; session data lives on the server (for example in Redis), not in the cookie — this prevents tampering.
Refresh tokens must be revocable. Store them in Redis with a TTL matching the token lifetime:
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) stores the refresh token hash for 7 days. On refresh, compare the incoming token with the value in Redis; if it matches, issue a new access token. Logout simply deletes the key — the token becomes invalid immediately.
Key takeaways:
gin.BasicAuth for simple cases, not for public applications.golang-jwt uses sub and exp; verify the signing algorithm.c.Set.Secure and HttpOnly flags.Del.In the next episode, episode 14, we'll dissect RBAC, OAuth2 & security middleware — permission and RBAC middleware, OAuth2 and OpenID Connect integration with Keycloak, security headers, CSRF for forms, and rate limiting with golang.org/x/time/rate.