Go: Go Middleware and JWT Authentication
Last updated: 2026-08-26
Middleware is the "pipeline" of Go microservices—authentication, rate limiting, logging, and recovery; each cross-cutting concern is a piece of وسيط.
When you need to verify JWT authentication, log activity, or enforce rate limits at every API endpoint, the وسيط pattern allows you to write the code once and have it apply everywhere.
1. You will learn
- JWT Generation and Validation
- Access Token + Refresh Token (Dual Tokens)
- RBAC (Role-Based Access Control)
- Token Bucket Throttling
- Recovery / Logging / CORS وسيط
- Cascading وسيط pipelines
2. A True Story of a Backend Engineer
(1) Pain point: Manually checking the token at each router; I forgot to do it three times.
Charlie is on the API Gateway team and is responsible for authentication for all endpoints:
"The boss required that all APIs validate JWT tokens. I copied and pasted 15 lines of token validation code into each handler. On the second day after deployment, I forgot to add authentication to
/admin/users—anyone could delete users directly. The CTO said, 'The security audit found a serious vulnerability.'"
// Bad code: manually check in every handler
func deleteUser(w http.ResponseWriter, r *http.Request) {
// Forgot to add Token verification! Serious security vulnerability
userID := r.PathValue("id")
deleteUserFromDB(userID)
}
func updateOrder(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
// Copy 15 lines of verification code each time
if !validateToken(token) {
http.Error(w, "unauthorized", 401)
return
}
// Business logic...
}
(2) Go Solution: Authentication Middleware
// Good code: auth وسيط, implement once, apply everywhere
func AuthMiddleware(jwtSecret سلسلة) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := extractToken(r)
claims, err := validateJWT(token, jwtSecret)
if err != nil {
writeError(w, http.StatusUnauthorized, "invalid token")
return
}
// Inject user info into Context
ctx := context.WithValue(r.Context(), "user", claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// Registration only needs one Middleware
mux.Handle("POST /api/orders", AuthMiddleware(secret)(orderHandler))
(3) Benefits: Manual vs. Middleware
| Dimension | Manual Check | Middleware Pattern |
|---|---|---|
| Risk of Omission | There is a risk of forgetting to add each new route | Zero |
| Code duplication | 15 lines per handler | 0 lines (handlers focus solely on business logic) |
| Modify logic | Modify 20 handlers | Modify 1 middleware |
| Unit Testing | Test authentication for each handler | Test the middleware only once |
3. JWT Authentication
▶ Example: JWT Generation and Validation
⚙️ Prerequisite: In production, use
go get github.com/golang-jwt/jwt/v5. The code below is a manual implementation for educational purposes only.
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
)
// ---------- JWT manual impl (no third-party dependency) ----------
type JWTClaims struct {
UserID int `json:"user_id"`
Role string `json:"role"`
Permissions []string `json:"permissions"`
ExpiresAt int64 `json:"exp"`
}
type JWTHeader struct {
Alg string `json:"alg"`
Typ string `json:"typ"`
}
func base64Encode(data []byte) string {
return strings.TrimRight(base64.URLEncoding.EncodeToString(data), "=")
}
func base64Decode(s string) ([]byte, error) {
// Pad to proper length
switch len(s) % 4 {
case 2:
s += "=="
case 3:
s += "="
}
return base64.URLEncoding.DecodeString(s)
}
func createJWT(claims JWTClaims, secret string) (string, error) {
header := JWTHeader{Alg: "HS256", Typ: "JWT"}
headerJSON, _ := json.Marshal(header)
claimsJSON, _ := json.Marshal(claims)
headerEnc := base64Encode(headerJSON)
claimsEnc := base64Encode(claimsJSON)
// Sign
message := headerEnc + "." + claimsEnc
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(message))
signature := base64Encode(mac.Sum(nil))
return message + "." + signature, nil
}
func validateJWT(token string, secret string) (*JWTClaims, error) {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid token format")
}
// Verify signature
message := parts[0] + "." + parts[1]
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(message))
expectedSig := base64Encode(mac.Sum(nil))
if !hmac.Equal([]byte(parts[2]), []byte(expectedSig)) {
return nil, fmt.Errorf("invalid signature")
}
// Parse claims
claimsJSON, err := base64Decode(parts[1])
if err != nil {
return nil, err
}
var claims JWTClaims
if err := json.Unmarshal(claimsJSON, &claims); err != nil {
return nil, err
}
// Check expiration
if time.Now().Unix() > claims.ExpiresAt {
return nil, fmt.Errorf("token expired")
}
return &claims, nil
}
func main() {
secret := "my-secret-key"
// Generate Token
claims := JWTClaims{
UserID: 1,
Role: "admin",
ExpiresAt: time.Now().Add(1 * time.Hour).Unix(),
}
token, _ := createJWT(claims, secret)
fmt.Printf("JWT: %s\n", token)
// Validate Token
validated, err := validateJWT(token, secret)
if err != nil {
fmt.Printf("Validation failed: %v\n", err)
} else {
fmt.Printf("Validation succeeded: user=%d, role=%s\n", validated.UserID, validated.Role)
}
}
github.com/golang-jwt/jwt/v5) instead of implementing it manually. The code above is intended solely to illustrate how JWTs work. Third-party libraries handle more edge cases (such as token types, key rotation, and standard claims verification).
4. Access Token + Refresh Token
▶ Example: Two-Token Authentication
package main
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
)
// ---------- Token Service ----------
type TokenService struct {
secret string
refreshTTL time.Duration
// Store refresh tokens (use Redis in production)
refreshTokens map[string]int // token → userID
mu sync.RWMutex
}
func NewTokenService(secret string) *TokenService {
return &TokenService{
secret: secret,
refreshTTL: 7 * 24 * time.Hour,
refreshTokens: make(map[string]int),
}
}
func (s *TokenService) GenerateTokens(userID int, role string) (accessToken, refreshToken string, err error) {
// Access Token (expires in 15 minutes)
accessClaims := JWTClaims{
UserID: userID,
Role: role,
ExpiresAt: time.Now().Add(15 * time.Minute).Unix(),
}
accessToken, err = createJWT(accessClaims, s.secret)
if err != nil {
return "", "", err
}
// Refresh Token (random string, expires in 7 days)
bytes := make([]byte, 32)
rand.Read(bytes)
refreshToken = hex.EncodeToString(bytes)
s.mu.Lock()
s.refreshTokens[refreshToken] = userID
s.mu.Unlock()
return accessToken, refreshToken, nil
}
func (s *TokenService) RefreshAccessToken(refreshToken string) (string, error) {
s.mu.RLock()
userID, exists := s.refreshTokens[refreshToken]
s.mu.RUnlock()
if !exists {
return "", fmt.Errorf("invalid refresh token")
}
// Generate new Access Token
claims := JWTClaims{
UserID: userID,
Role: "user",
ExpiresAt: time.Now().Add(15 * time.Minute).Unix(),
}
return createJWT(claims, s.secret)
}
func (s *TokenService) RevokeRefreshToken(refreshToken string) {
s.mu.Lock()
delete(s.refreshTokens, refreshToken)
s.mu.Unlock()
}
// ---------- Auth Handler ----------
type AuthHandler struct {
tokenSvc *TokenService
}
type loginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type tokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
}
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
var req loginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
return
}
// Verify username and password (use bcrypt in production)
if req.Username != "admin" || req.Password != "password" {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
return
}
accessToken, refreshToken, err := h.tokenSvc.GenerateTokens(1, "admin")
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "token generation failed"})
return
}
writeJSON(w, http.StatusOK, tokenResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
TokenType: "Bearer",
ExpiresIn: 900, // 15 minutes
})
}
func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
var req struct {
RefreshToken string `json:"refresh_token"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
return
}
accessToken, err := h.tokenSvc.RefreshAccessToken(req.RefreshToken)
if err != nil {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid refresh token"})
return
}
writeJSON(w, http.StatusOK, map[string]string{"access_token": accessToken})
}
func main() {
tokenSvc := NewTokenService("my-secret-key")
authHandler := &AuthHandler{tokenSvc: tokenSvc}
mux := http.NewServeMux()
mux.HandleFunc("POST /login", authHandler.Login)
mux.HandleFunc("POST /refresh", authHandler.Refresh)
fmt.Println("Auth service listening on :8080")
http.ListenAndServe(":8080", mux)
}
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
sequenceDiagram
participant Client
participant API as API Gateway
participant Auth as Auth Service
Client->>API: POST /login (username, password)
API->>Auth: Verify credentials
Auth-->>API: access_token (15min) + refresh_token (7d)
API-->>Client: Return dual tokens
Client->>API: GET /orders (Bearer access_token)
API->>API: Validate access_token
API-->>Client: 200 OK
Client->>API: GET /orders (access_token expired)
API-->>Client: 401 Unauthorized
Client->>API: POST /refresh (refresh_token)
API->>Auth: Validate refresh_token
Auth-->>API: New access_token
API-->>Client: Return new access_token
5. RBAC Permissions Middleware
▶ Example: RBAC Implementation
package main
import (
"context"
"encoding/json"
"net/http"
)
type UserRole سلسلة
const (
RoleAdmin UserRole = "admin"
RoleUser UserRole = "user"
RoleGuest UserRole = "guest"
)
// Permission definitions
var rolePermissions = map[UserRole][]سلسلة{
RoleAdmin: {"read:users", "write:users", "delete:users", "read:orders", "write:orders"},
RoleUser: {"read:orders", "write:orders"},
RoleGuest: {"read:products"},
}
type Claims struct {
UserID int
Role UserRole
}
// Get claims from Context
func GetClaims(r *http.Request) *Claims {
claims, _ := r.Context().Value("claims").(*Claims)
return claims
}
// RBAC وسيط
func RequirePermission(permission سلسلة) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims := GetClaims(r)
if claims == nil {
writeError(w, http.StatusUnauthorized, "not authenticated")
return
}
permissions, exists := rolePermissions[claims.Role]
if !exists {
writeError(w, http.StatusForbidden, "no permissions defined for role")
return
}
// Check permission
hasPermission := false
for _, p := range permissions {
if p == permission {
hasPermission = true
break
}
}
if !hasPermission {
writeError(w, http.StatusForbidden, "insufficient permissions")
return
}
next.ServeHTTP(w, r)
})
}
}
// ---------- Route registration ----------
func setupRoutes() http.Handler {
mux := http.NewServeMux()
// Public endpoints
mux.HandleFunc("POST /login", loginHandler)
// Authenticated endpoints
mux.Handle("GET /api/orders", RequirePermission("read:orders")(authMiddleware(http.HandlerFunc(listOrders))))
mux.Handle("POST /api/orders", RequirePermission("write:orders")(authMiddleware(http.HandlerFunc(createOrder))))
mux.Handle("DELETE /api/users/{id}", RequirePermission("delete:users")(authMiddleware(http.HandlerFunc(deleteUser))))
return mux
}
(2) Middleware Pipeline
Request → Auth Middleware → RBAC Middleware → Handler
| |
Verify JWT Token Check Role permissions
Inject Claims User has permission?
| Level | وسيط | Responsibilities |
|---|---|---|
| 1 | Recovery | Catch panic, return 500 |
| 2 | Logging | Log طلب duration |
| 3 | CORS | Handle Cross-Origin Requests |
| 4 | Auth | Verify JWT, inject user information |
| 5 | RBAC | Check Role Permissions |
| 6 | RateLimit | Token Bucket Throttling |
| 7 | Handler | Business Logic |
6. Token Bucket Throttling
▶ Example: Token Bucket Implementation
package main
import (
"net/http"
"sync"
"time"
)
type TokenBucket struct {
mu sync.Mutex
tokens float64
maxTokens float64
refillRate float64
lastRefill time.Time
}
func NewTokenBucket(rate float64, burst int) *TokenBucket {
return &TokenBucket{
tokens: float64(burst),
maxTokens: float64(burst),
refillRate: rate,
lastRefill: time.Now(),
}
}
func (tb *TokenBucket) Allow() bool {
tb.mu.Lock()
defer tb.mu.Unlock()
// Refill tokens
now := time.Now()
elapsed := now.Sub(tb.lastRefill).Seconds()
tb.tokens = min(tb.tokens+elapsed*tb.refillRate, tb.maxTokens)
tb.lastRefill = now
// Take a token
if tb.tokens >= 1 {
tb.tokens--
return true
}
return false
}
func min(a, b float64) float64 {
if a < b {
return a
}
return b
}
// IP rate limiter
type IPRateLimiter struct {
mu sync.RWMutex
buckets map[string]*TokenBucket
rate float64
burst int
}
func NewIPRateLimiter(rate float64, burst int) *IPRateLimiter {
return &IPRateLimiter{
buckets: make(map[string]*TokenBucket),
rate: rate,
burst: burst,
}
}
func (rl *IPRateLimiter) GetBucket(ip string) *TokenBucket {
rl.mu.Lock()
defer rl.mu.Unlock()
bucket, exists := rl.buckets[ip]
if !exists {
bucket = NewTokenBucket(rl.rate, rl.burst)
rl.buckets[ip] = bucket
}
return bucket
}
// RateLimiterMiddleware
func RateLimiterMiddleware(rate float64, burst int) Middleware {
limiter := NewIPRateLimiter(rate, burst)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
bucket := limiter.GetBucket(ip)
if !bucket.Allow() {
w.Header().Set("Retry-After", "1")
writeError(w, http.StatusTooManyRequests, "rate limit exceeded")
return
}
next.ServeHTTP(w, r)
})
}
}
7. Complete Example: API Gateway Middleware Pipeline
▶ Example: Full Pipeline
// api_gateway.go
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
)
// ---------- Type definitions ----------
type Middleware func(http.Handler) http.Handler
type Claims struct {
UserID int
Role سلسلة
}
// ---------- Middleware ----------
// Recovery
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("[PANIC] %v", err)
writeError(w, http.StatusInternalServerError, "internal خطأ")
}
}()
next.ServeHTTP(w, r)
})
}
// Logging
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
next.ServeHTTP(w, r)
log.Printf("[%s] %s → %v", r.Method, r.URL.Path, time.Since(start))
})
}
// CORS
func CORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// Auth (JWT authentication)
func Auth(secret سلسلة) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if !strings.HasPrefix(authHeader, "Bearer ") {
writeError(w, http.StatusUnauthorized, "missing token")
return
}
token := authHeader[7:]
claims, err := validateJWT(token, secret)
if err != nil {
writeError(w, http.StatusUnauthorized, err.Error())
return
}
ctx := context.WithValue(r.Context(), "claims", &Claims{
UserID: claims.UserID,
Role: claims.Role,
})
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// RBAC
func RequireRole(roles ...سلسلة) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, ok := r.Context().Value("claims").(*Claims)
if !ok {
writeError(w, http.StatusUnauthorized, "not authenticated")
return
}
for _, role := range roles {
if claims.Role == role {
next.ServeHTTP(w, r)
return
}
}
writeError(w, http.StatusForbidden, "insufficient permissions")
})
}
}
// Rate Limiter
var rateLimiter = NewIPRateLimiter(10, 20) // 10 req/s, burst 20
func RateLimit(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
bucket := rateLimiter.GetBucket(ip)
if !bucket.Allow() {
writeError(w, http.StatusTooManyRequests, "rate limit exceeded")
return
}
next.ServeHTTP(w, r)
})
}
// ---------- Chain ----------
func Chain(h http.Handler, mws ...Middleware) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
// ---------- Handlers ----------
type OrderHandler struct{}
func (h *OrderHandler) ListOrders(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[سلسلة]سلسلة{"orders": "list"})
}
func (h *OrderHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusCreated, map[سلسلة]سلسلة{"order": "created"})
}
type AdminHandler struct{}
func (h *AdminHandler) DeleteUser(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[سلسلة]سلسلة{"deleted": "user"})
}
// ---------- Utility functions ----------
func writeJSON(w http.ResponseWriter, حالة int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(حالة)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, حالة int, msg سلسلة) {
writeJSON(w, حالة, map[سلسلة]سلسلة{"خطأ": msg})
}
// ---------- Main ----------
func main() {
jwtSecret := "super-secret-key"
orders := &OrderHandler{}
mux := http.NewServeMux()
// Public endpoint
mux.HandleFunc("POST /login", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[سلسلة]سلسلة{"token": "login-token"})
})
// Authenticated + rate-limited endpoints
mux.Handle("GET /api/orders", Chain(
http.HandlerFunc(orders.ListOrders),
Auth(jwtSecret),
RateLimit,
))
mux.Handle("POST /api/orders", Chain(
http.HandlerFunc(orders.CreateOrder),
Auth(jwtSecret),
RateLimit,
))
// Admin endpoints (Auth + RBAC + RateLimit)
mux.Handle("DELETE /api/users/{id}", Chain(
http.HandlerFunc((&AdminHandler{}).DeleteUser),
Auth(jwtSecret),
RequireRole("admin"),
RateLimit,
))
// Global وسيط (Recovery → Logging → CORS → routes)
app := Chain(mux, Recovery, Logging, CORS)
خادم := &http.Server{Addr: ":8080", Handler: app}
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("Shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
خادم.Shutdown(ctx)
}()
log.Println("API Gateway listening on :8080")
if err := خادم.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}
❓ FAQ
github.com/golang-jwt/jwt/v5. Access tokens are short-lived (15 minutes), while refresh tokens are long-lived (7 days) and stored on the server.rolePermissions[role] = []permission + the middleware checks permission in rolePermissions[claims.Role].debug.Stack() prints the stack trace. Use the ERROR log level. In production, do not return the stack trace to the client—log it only on the server.Chain(h, A, B, C) → The request passes through A → B → C → Handler → C → B → A. Registration order = from the outermost layer to the innermost layer. Recovery is at the outermost layer (captures panics thrown by any layer), and Handler is at the innermost layer (business logic).📖 Summary
- JWT: Header + Payload + Signature,
golang-jwt/jwt/v5 - Two tokens: Access (15 min) + Refresh (7 days)
- Auth middleware: Verifies the token and injects claims into the context
- RBAC: Role → Permission mapping; middleware checks permissions
- Token Bucket Throttling: Allows spikes, controls average flow
- Recovery middleware: Captures a panic and returns a 500
- Middleware order: Recovery → Logging → CORS → Auth → RBAC → RateLimit → Handler
📝 Exercises
-
Basic (Difficulty ⭐): Implement a simple authentication middleware. Create a
/api/protectedendpoint that requires the request header to includeAuthorization: Bearer token123. If the token is valid, return{"message": "access granted"}; otherwise, return a 401 error. -
Advanced (Difficulty ⭐⭐): Implement a complete JWT authentication system. Requirements: (1)
POST /registerfor registration (password encrypted with bcrypt); (2)POST /loginfor login (returns access_token + refresh_token); (3) An Auth middleware that validates the access_token for all/api/*endpoints; (4)POST /refreshto exchange therefresh_tokenfor a newaccess_token; (5)POST /logoutto revoke therefresh_token. -
Challenge (Difficulty ⭐⭐⭐): Implement a blog management API with RBAC and rate limiting. Requirements: (1) Three roles: admin, editor, and reader; (2) admin: full permissions; (3) editor: create, edit, and delete their own posts; (4) reader: read-only access; (5) Each role has different rate limits; (6) All endpoints must pass through the following middleware pipeline: Recovery → Logging → Auth → RBAC → RateLimit → Handler; (7) Use
-raceto verify concurrency safety.