JWT Security Best Practices for Go Applications
In today's digital landscape, securing your applications is more critical than ever. JSON Web Tokens (JWTs) have become a popular choice for authentication and authorization, but they come with their own set of security considerations. In this post, we'll explore essential JWT security best practices specifically tailored for Go applications.
Understanding JWT Security Risksโ
JWTs are stateless and can be vulnerable to several security threats if not implemented correctly:
- Token Theft: JWTs can be intercepted if transmitted over insecure channels
- Token Replay: Expired tokens might be reused if proper validation is missing
- Algorithm Confusion: Attackers might try to use different signing algorithms
- Secret Exposure: Weak or exposed secrets can compromise the entire system
Best Practices Implementationโ
1. Use Strong Signing Algorithmsโ
Always use strong, asymmetric algorithms like RS256 or ES256 instead of symmetric algorithms like HS256 for production applications:
// Good: Use RS256 for production
claims := jwt.MapClaims{
"user_id": user.ID,
"exp": time.Now().Add(time.Hour * 24).Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
tokenString, err := token.SignedString(privateKey)
// Avoid: HS256 in production
// token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
2. Implement Proper Token Expirationโ
Set reasonable expiration times and implement refresh token mechanisms:
type TokenPair struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
}
func GenerateTokenPair(user *User) (*TokenPair, error) {
// Access token: short-lived (15 minutes)
accessClaims := jwt.MapClaims{
"user_id": user.ID,
"exp": time.Now().Add(time.Minute * 15).Unix(),
"type": "access",
}
// Refresh token: longer-lived (7 days)
refreshClaims := jwt.MapClaims{
"user_id": user.ID,
"exp": time.Now().AddDate(0, 0, 7).Unix(),
"type": "refresh",
}
// Implementation details...
}
3. Validate Token Claimsโ
Always validate all claims, especially expiration and issuer:
func ValidateToken(tokenString string, publicKey *rsa.PublicKey) (*jwt.Token, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Validate algorithm
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return publicKey, nil
})
if err != nil {
return nil, err
}
// Validate claims
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
// Check expiration
if exp, ok := claims["exp"].(float64); ok {
if time.Unix(int64(exp), 0).Before(time.Now()) {
return nil, fmt.Errorf("token expired")
}
}
// Check issuer (if applicable)
if iss, ok := claims["iss"].(string); ok {
if iss != "goauth-service" {
return nil, fmt.Errorf("invalid issuer")
}
}
return token, nil
}
return nil, fmt.Errorf("invalid token")
}
4. Secure Token Storageโ
Store tokens securely on the client side:
// Server-side: Set secure cookie attributes
func SetSecureCookie(w http.ResponseWriter, name, value string) {
cookie := &http.Cookie{
Name: name,
Value: value,
Path: "/",
HttpOnly: true,
Secure: true, // HTTPS only
SameSite: http.SameSiteStrictMode,
MaxAge: 3600, // 1 hour
}
http.SetCookie(w, cookie)
}
5. Implement Rate Limitingโ
Protect your JWT endpoints from brute force attacks:
func RateLimitMiddleware(next http.Handler) http.Handler {
limiter := rate.NewLimiter(rate.Every(time.Second), 10) // 10 requests per second
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "Too many requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
GoAuth Library Featuresโ
Our GoAuth library implements these best practices out of the box:
- Automatic algorithm validation with configurable allowed methods
- Built-in claim validation including expiration and issuer checks
- Secure token generation with proper entropy
- Rate limiting support for authentication endpoints
- Comprehensive logging for security monitoring
Example Implementationโ
Here's how to use GoAuth with these security practices:
package main
import (
"log"
"net/http"
"github.com/your-org/goauth"
"github.com/your-org/goauth/middleware"
)
func main() {
// Initialize GoAuth with secure defaults
auth := goauth.New(&goauth.Config{
Algorithm: "RS256",
AccessTokenTTL: time.Minute * 15,
RefreshTokenTTL: time.Hour * 24 * 7,
Issuer: "goauth-service",
RateLimit: true,
RateLimitPerSec: 10,
})
// Apply security middleware
http.HandleFunc("/auth/login", middleware.RateLimit(auth.LoginHandler))
http.HandleFunc("/auth/refresh", middleware.RateLimit(auth.RefreshHandler))
log.Fatal(http.ListenAndServe(":8080", nil))
}
Security Checklistโ
Before deploying your JWT implementation, ensure you've covered:
- Strong signing algorithms (RS256/ES256)
- Proper token expiration times
- Claim validation (exp, iat, iss, aud)
- Secure token storage (HttpOnly cookies)
- Rate limiting on auth endpoints
- HTTPS enforcement
- Regular secret rotation
- Security monitoring and logging
Conclusionโ
JWT security requires careful attention to implementation details. By following these best practices and using the GoAuth library, you can build secure, production-ready authentication systems in Go.
Remember: Security is not a one-time setup but an ongoing process. Stay updated with the latest security recommendations and regularly audit your JWT implementation.
For more security insights and updates, follow our blog and join our community discussions on GitHub.
