main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
package main

import (
	"context"
	"crypto/subtle"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

type JWTConfig struct {
	SecretKey     []byte
	TokenDuration time.Duration
	Issuer        string
}

type CustomClaims struct {
	UserID string `json:"user_id"`
	Role   string `json:"role"`
	jwt.RegisteredClaims
}

type claimsKey struct{}

func writeJSON(w http.ResponseWriter, status int, v interface{}) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(v)
}

func bearerToken(r *http.Request) (string, error) {
	h := r.Header.Get("Authorization")
	if h == "" {
		return "", errors.New("missing Authorization header")
	}
	parts := strings.Fields(h)
	if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
		return "", errors.New("invalid Authorization header (use: Bearer <token>)")
	}
	return parts[1], nil
}

func JWTMiddleware(cfg JWTConfig) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			tokenString, err := bearerToken(r)
			if err != nil {
				writeJSON(w, http.StatusUnauthorized, map[string]string{"error": err.Error()})
				return
			}

			claims := &CustomClaims{}
			token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
				if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
					return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
				}
				return cfg.SecretKey, nil
			})
			if err != nil {
				if errors.Is(err, jwt.ErrTokenExpired) {
					writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "token expired"})
					return
				}
				writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid token"})
				return
			}
			if !token.Valid {
				writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid token"})
				return
			}

			ctx := context.WithValue(r.Context(), claimsKey{}, claims)
			next.ServeHTTP(w, r.WithContext(ctx))
		})
	}
}

func RequireRole(requiredRole string) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			claims, ok := r.Context().Value(claimsKey{}).(*CustomClaims)
			if !ok || claims == nil {
				writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "missing claims"})
				return
			}

			if subtle.ConstantTimeCompare([]byte(claims.Role), []byte(requiredRole)) != 1 {
				writeJSON(w, http.StatusForbidden, map[string]string{"error": "insufficient permissions"})
				return
			}

			next.ServeHTTP(w, r)
		})
	}
}

func GenerateToken(cfg JWTConfig, userID, role string) (string, error) {
	now := time.Now()
	claims := CustomClaims{
		UserID: userID,
		Role:   role,
		RegisteredClaims: jwt.RegisteredClaims{
			Issuer:    cfg.Issuer,
			IssuedAt:  jwt.NewNumericDate(now),
			ExpiresAt: jwt.NewNumericDate(now.Add(cfg.TokenDuration)),
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(cfg.SecretKey)
}

func main() {
	cfg := JWTConfig{
		SecretKey:     []byte("your-strong-secret-key-keep-it-safe-12345"),
		TokenDuration: 24 * time.Hour,
		Issuer:        "example-app",
	}

	adminToken, err := GenerateToken(cfg, "user_123", "admin")
	if err != nil {
		log.Fatalf("generate token: %v", err)
	}
	userToken, _ := GenerateToken(cfg, "user_456", "user")
	log.Printf("Admin token (Authorization: Bearer <token>): %s", adminToken)
	log.Printf("User  token (Authorization: Bearer <token>): %s", userToken)

	mux := http.NewServeMux()
	mux.HandleFunc("/public", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(w, http.StatusOK, map[string]string{"message": "public endpoint"})
	})

	mux.Handle("/protected", JWTMiddleware(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		claims := r.Context().Value(claimsKey{}).(*CustomClaims)
		writeJSON(w, http.StatusOK, map[string]interface{}{
			"message": "protected endpoint",
			"user_id": claims.UserID,
			"role":    claims.Role,
		})
	})))

	mux.Handle("/admin", JWTMiddleware(cfg)(RequireRole("admin")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		claims := r.Context().Value(claimsKey{}).(*CustomClaims)
		writeJSON(w, http.StatusOK, map[string]interface{}{
			"message": "admin endpoint",
			"user_id": claims.UserID,
		})
	}))))

	log.Println("Server starting on :8080")
	log.Println("Try:")
	log.Println("  curl http://localhost:8080/public")
	log.Println("  curl -H "Authorization: Bearer <TOKEN>" http://localhost:8080/protected")
	log.Fatal(http.ListenAndServe(":8080", mux))
}

How It Works

HTTP middleware that parses, validates, and enforces JWTs with HMAC signing and role-based checks.

Reads Authorization headers, strips Bearer tokens, validates signature and expiry with a shared key, extracts claims, optionally checks roles, and injects user info into the request context for downstream handlers.

Key Concepts

  • 1Uses HMAC-SHA256 signing with configurable secret and expiration.
  • 2Gracefully handles malformed, expired, or missing tokens with 401 or 403 responses.
  • 3Context enrichment makes user claims available to handlers.

When to Use This Pattern

  • Protecting API routes in microservices.
  • Adding session-less authentication to SPAs or mobile backends.
  • Gateway or middleware enforcing role-based access before business logic.

Best Practices

  • Store secrets securely (environment or KMS) and rotate regularly.
  • Validate issuer and audience if tokens come from external identity providers.
  • Set short expirations and require refresh flows for long-lived sessions.
Go Version1.16
Difficultyintermediate
Production ReadyYes
Lines of Code162