Building a Secure JWT Issuer in Go: A Complete Guide
JSON Web Tokens (JWT) have become the de facto standard for implementing stateless authentication in modern web applications. In this guide, we’ll implement a secure JWT issuer in Go, covering both basic implementation and advanced security considerations.
Understanding JWT Basics
A JWT consists of three parts: header, payload, and signature. These parts are Base64URL encoded and concatenated with dots. The signature ensures the token hasn’t been tampered with, while the payload carries the claims (data) we want to transmit securely.
First, let’s create a basic JWT issuer that can generate tokens with custom claims.
package main
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
UserID string `json:"uid"`
Username string `json:"username"`
Roles []string `json:"roles"`
jwt.RegisteredClaims
}
type JWTIssuer struct {
secretKey []byte
duration time.Duration
}
func NewJWTIssuer(secretKey string, duration time.Duration) *JWTIssuer {
return &JWTIssuer{
secretKey: []byte(secretKey),
duration: duration,
}
}
func (i *JWTIssuer) IssueToken(userID, username string, roles []string) (string, error) {
now := time.Now()
claims := Claims{
UserID: userID,
Username: username,
Roles: roles,
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(i.duration)),
NotBefore: jwt.NewNumericDate(now),
Issuer: "your-service-name",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(i.secretKey)
}
Basic Usage Example
Here’s how to use the basic JWT issuer:
func main() {
// Create a new issuer with a 24-hour token duration
issuer := NewJWTIssuer("your-secret-key", 24*time.Hour)
// Issue a token
token, err := issuer.IssueToken(
"user123",
"john.doe",
[]string{"user", "admin"},
)
if err != nil {
panic(err)
}
fmt.Println("Generated Token:", token)
}
Adding Token Validation
It’s crucial to implement token validation to ensure the tokens we receive are valid and haven’t been tampered with.
func (i *JWTIssuer) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
// Validate the signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return i.secretKey, nil
})
if err != nil {
return nil, fmt.Errorf("invalid token: %w", err)
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("invalid token claims")
}
Enhanced Security Features
Let’s add some security enhancements to our JWT issuer.
Token Revocation Support
We’ll implement a simple token blacklist using Redis to support token revocation:
import (
"context"
"github.com/redis/go-redis/v9"
)
type EnhancedJWTIssuer struct {
*JWTIssuer
redis *redis.Client
}
func NewEnhancedJWTIssuer(secretKey string, duration time.Duration, redisURL string) (*EnhancedJWTIssuer, error) {
opt, err := redis.ParseURL(redisURL)
if err != nil {
return nil, err
}
return &EnhancedJWTIssuer{
JWTIssuer: NewJWTIssuer(secretKey, duration),
redis: redis.NewClient(opt),
}, nil
}
func (i *EnhancedJWTIssuer) RevokeToken(ctx context.Context, token string) error {
claims, err := i.ValidateToken(token)
if err != nil {
return err
}
// Store the token in the blacklist until its original expiration
expiration := time.Until(claims.ExpiresAt.Time)
return i.redis.Set(ctx, "blacklist:"+token, true, expiration).Err()
}
func (i *EnhancedJWTIssuer) IsTokenRevoked(ctx context.Context, token string) bool {
exists, err := i.redis.Exists(ctx, "blacklist:"+token).Result()
return err == nil && exists > 0
}
Rate Limiting Token Generation
We’ll add rate limiting to prevent token generation abuse:
import (
"golang.org/x/time/rate"
"sync"
)
type RateLimitedJWTIssuer struct {
*EnhancedJWTIssuer
limiters map[string]*rate.Limiter
limitersMu sync.RWMutex
}
func NewRateLimitedJWTIssuer(secretKey string, duration time.Duration, redisURL string) (*RateLimitedJWTIssuer, error) {
enhanced, err := NewEnhancedJWTIssuer(secretKey, duration, redisURL)
if err != nil {
return nil, err
}
return &RateLimitedJWTIssuer{
EnhancedJWTIssuer: enhanced,
limiters: make(map[string]*rate.Limiter),
}, nil
}
func (i *RateLimitedJWTIssuer) getLimiter(userID string) *rate.Limiter {
i.limitersMu.RLock()
limiter, exists := i.limiters[userID]
i.limitersMu.RUnlock()
if exists {
return limiter
}
i.limitersMu.Lock()
defer i.limitersMu.Unlock()
limiter = rate.NewLimiter(rate.Every(time.Minute), 10) // 10 tokens per minute
i.limiters[userID] = limiter
return limiter
}
func (i *RateLimitedJWTIssuer) IssueTokenWithRateLimit(ctx context.Context, userID, username string, roles []string) (string, error) {
limiter := i.getLimiter(userID)
if !limiter.Allow() {
return "", fmt.Errorf("rate limit exceeded for user %s", userID)
}
return i.IssueToken(userID, username, roles)
}
Using the Enhanced JWT Issuer in a Web Application
Here’s an example of how to use our enhanced JWT issuer in a web application:
func main() {
issuer, err := NewRateLimitedJWTIssuer(
"your-secret-key",
24*time.Hour,
"redis://localhost:6379/0",
)
if err != nil {
panic(err)
}
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
// Authentication logic here...
userID := "user123"
username := "john.doe"
roles := []string{"user", "admin"}
token, err := issuer.IssueTokenWithRateLimit(r.Context(), userID, username, roles)
if err != nil {
http.Error(w, err.Error(), http.StatusTooManyRequests)
return
}
json.NewEncoder(w).Encode(map[string]string{
"token": token,
})
})
http.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "no token provided", http.StatusBadRequest)
return
}
// Remove "Bearer " prefix if present
token = strings.TrimPrefix(token, "Bearer ")
if err := issuer.RevokeToken(r.Context(), token); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
})
http.ListenAndServe(":8080", nil)
}
Best Practices and Security Considerations
Secret Key Management
- Never hardcode secret keys
- Use environment variables or secure key management services
- Rotate keys periodically
Token Expiration
- Set reasonable expiration times
- Consider using refresh tokens for long-term sessions
- Implement token rotation for sensitive operations
Claims Security
- Include only necessary information in claims
- Avoid sensitive data in tokens
- Use standard claims when possible
Validation
- Always validate tokens before trusting them
- Check signature, expiration, and issuer
- Implement proper error handling
Conclusion
Implementing a JWT issuer in Go requires careful consideration of security aspects beyond just token generation. By including features like token revocation, rate limiting, and proper validation, we can create a robust authentication system that’s both secure and scalable.
The implementation provided here serves as a foundation that you can build upon based on your specific requirements. Remember to always follow security best practices and keep your dependencies updated to maintain a secure system.