JWT Authentication Middleware for HTTP Servers
HTTP middleware that parses, validates, and enforces JWTs with HMAC signing and role-based checks.
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