How to Implement JWT Authentication in Go
JSON Web Tokens (JWT) solve a fundamental problem in distributed systems: how do you authenticate users without maintaining server-side session state? A JWT is a self-contained token with three parts...
Key Insights
- JWT authentication enables stateless, scalable authentication by encoding user data directly in cryptographically signed tokens that clients store and send with each request
- The golang-jwt/jwt library provides production-ready token generation and validation, but you must implement proper expiration handling, secure secret management, and refresh token logic yourself
- Middleware-based authentication in Go allows you to protect entire route groups with minimal code duplication while maintaining clean separation between auth logic and business logic
Understanding JWT and Why It Matters
JSON Web Tokens (JWT) solve a fundamental problem in distributed systems: how do you authenticate users without maintaining server-side session state? A JWT is a self-contained token with three parts separated by dots: a header (algorithm and token type), a payload (claims about the user), and a signature (cryptographic verification). When a user logs in, your server generates a signed token containing their identity and permissions. The client stores this token and includes it in subsequent requests. Your server validates the signature to confirm the token hasn’t been tampered with, then extracts user information directly from the payload—no database lookup required.
This stateless approach scales horizontally because any server instance can validate any token without coordinating with other servers or hitting a shared session store. It’s ideal for microservices, mobile apps, and SPAs where traditional cookie-based sessions create friction.
Setting Up Your Go Project
Start by initializing a new Go module and installing the necessary dependencies:
mkdir jwt-auth-demo
cd jwt-auth-demo
go mod init github.com/yourusername/jwt-auth-demo
go get github.com/golang-jwt/jwt/v5
go get github.com/gorilla/mux
Structure your project like this:
jwt-auth-demo/
├── main.go
├── handlers/
│ ├── auth.go
│ └── protected.go
├── middleware/
│ └── auth.go
└── utils/
└── jwt.go
This separation keeps authentication logic isolated from your HTTP handlers and makes testing easier.
Generating JWT Tokens
Create utils/jwt.go to handle token generation. First, define a custom claims struct that embeds the standard JWT claims:
package utils
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
var jwtSecret = []byte("your-256-bit-secret-change-this-in-production")
type Claims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
Role string `json:"role"`
jwt.RegisteredClaims
}
func GenerateToken(userID uint, username, role string) (string, error) {
expirationTime := time.Now().Add(15 * time.Minute)
claims := &Claims{
UserID: userID,
Username: username,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "your-app-name",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtSecret)
if err != nil {
return "", err
}
return tokenString, nil
}
This function creates a token that expires in 15 minutes and includes both standard claims (expiration, issuer) and custom claims (user ID, username, role). The short expiration time limits the damage if a token is compromised. We’ll implement refresh tokens later to handle longer sessions.
Validating and Parsing Tokens
Token validation is where security happens. Create utils/jwt.go (continued):
func ValidateToken(tokenString string) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
// Verify the signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return jwtSecret, nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, jwt.ErrSignatureInvalid
}
return claims, nil
}
Now create middleware in middleware/auth.go that intercepts requests and validates tokens:
package middleware
import (
"context"
"net/http"
"strings"
"jwt-auth-demo/utils"
)
type contextKey string
const ClaimsContextKey contextKey = "claims"
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Authorization header required", http.StatusUnauthorized)
return
}
// Expected format: "Bearer <token>"
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
return
}
claims, err := utils.ValidateToken(parts[1])
if err != nil {
http.Error(w, "Invalid or expired token", http.StatusUnauthorized)
return
}
// Add claims to request context for handlers to use
ctx := context.WithValue(r.Context(), ClaimsContextKey, claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
This middleware extracts the token from the Authorization header, validates it, and adds the claims to the request context so downstream handlers can access user information.
Building Login and Protected Routes
Create handlers/auth.go for the login endpoint:
package handlers
import (
"encoding/json"
"net/http"
"jwt-auth-demo/utils"
)
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type LoginResponse struct {
Token string `json:"token"`
}
func Login(w http.ResponseWriter, r *http.Request) {
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// In production, verify against database with hashed passwords
if req.Username != "demo" || req.Password != "password123" {
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
return
}
// Generate token for authenticated user
token, err := utils.GenerateToken(1, req.Username, "user")
if err != nil {
http.Error(w, "Failed to generate token", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(LoginResponse{Token: token})
}
Create handlers/protected.go to demonstrate accessing user information:
package handlers
import (
"encoding/json"
"net/http"
"jwt-auth-demo/middleware"
"jwt-auth-demo/utils"
)
func ProtectedEndpoint(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value(middleware.ClaimsContextKey).(*utils.Claims)
response := map[string]interface{}{
"message": "You have access to this protected resource",
"user_id": claims.UserID,
"username": claims.Username,
"role": claims.Role,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
Wire everything together in main.go:
package main
import (
"log"
"net/http"
"jwt-auth-demo/handlers"
"jwt-auth-demo/middleware"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
// Public routes
r.HandleFunc("/login", handlers.Login).Methods("POST")
// Protected routes
protected := r.PathPrefix("/api").Subrouter()
protected.Use(middleware.AuthMiddleware)
protected.HandleFunc("/profile", handlers.ProtectedEndpoint).Methods("GET")
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", r))
}
Implementing Token Refresh
Short-lived access tokens improve security, but forcing users to re-login every 15 minutes creates terrible UX. Refresh tokens solve this. Add to utils/jwt.go:
func GenerateRefreshToken(userID uint) (string, error) {
expirationTime := time.Now().Add(7 * 24 * time.Hour) // 7 days
claims := &Claims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
Add a refresh endpoint in handlers/auth.go:
type RefreshRequest struct {
RefreshToken string `json:"refresh_token"`
}
func Refresh(w http.ResponseWriter, r *http.Request) {
var req RefreshRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
claims, err := utils.ValidateToken(req.RefreshToken)
if err != nil {
http.Error(w, "Invalid refresh token", http.StatusUnauthorized)
return
}
// Generate new access token
newToken, err := utils.GenerateToken(claims.UserID, claims.Username, claims.Role)
if err != nil {
http.Error(w, "Failed to generate token", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(LoginResponse{Token: newToken})
}
In production, store refresh tokens in a database with the ability to revoke them. This gives you the security benefits of short-lived access tokens with the UX of long-lived sessions.
Security Best Practices
Never hardcode JWT secrets. Use environment variables:
var jwtSecret = []byte(os.Getenv("JWT_SECRET"))
Generate secrets with at least 256 bits of entropy. Always use HTTPS in production—JWTs in HTTP headers are vulnerable to interception. Set appropriate token expiration times: 15 minutes for access tokens, 7 days for refresh tokens. Store tokens in memory or httpOnly cookies on the client side, never in localStorage where they’re accessible to XSS attacks.
Consider adding a token blacklist for logout functionality. When users log out, add their token to a Redis set with TTL matching the token’s expiration. Check this blacklist in your validation middleware.
Testing Your Implementation
Create utils/jwt_test.go:
package utils
import (
"testing"
"time"
)
func TestGenerateAndValidateToken(t *testing.T) {
token, err := GenerateToken(1, "testuser", "admin")
if err != nil {
t.Fatalf("Failed to generate token: %v", err)
}
claims, err := ValidateToken(token)
if err != nil {
t.Fatalf("Failed to validate token: %v", err)
}
if claims.UserID != 1 || claims.Username != "testuser" {
t.Errorf("Claims mismatch: got %+v", claims)
}
}
func TestExpiredToken(t *testing.T) {
// Create token that expires immediately
claims := &Claims{
UserID: 1,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(-1 * time.Hour)),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, _ := token.SignedString(jwtSecret)
_, err := ValidateToken(tokenString)
if err == nil {
t.Error("Expected validation to fail for expired token")
}
}
Run tests with go test ./... to verify your implementation handles both valid and invalid tokens correctly.
JWT authentication in Go is straightforward once you understand the pieces: generate signed tokens on login, validate them in middleware, and extract user information from claims. This pattern scales effortlessly and integrates cleanly with any HTTP router. The key is implementing proper security measures around secret management, token expiration, and HTTPS enforcement.