JSON Web Tokens (JWTs) are a popular mechanism for authentication and authorization in modern web applications. However, proper JWT verification is crucial for security. Let’s explore how to implement robust JWT verification in Go.
Understanding JWT Structure
A JWT consists of three parts:
- Header (algorithm and token type)
- Payload (claims)
- Signature
Each part is base64-encoded and separated by dots. For example:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Essential Claims
Standard JWT claims include:
sub
(Subject): Identifier for the token subjectexp
(Expiration Time): Token expiry timestampiat
(Issued At): Token creation timestampiss
(Issuer): Token issueraud
(Audience): Intended token recipient
Implementing JWT Verification
Here’s a comprehensive example of JWT verification in Go:
package main
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v4"
)
type CustomClaims struct {
jwt.RegisteredClaims
UserRole string `json:"role"`
}
func verifyToken(tokenString string) (*CustomClaims, error) {
// Parse the token with custom claims
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
// Verify signing algorithm
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
// Return the secret key used for signing
return []byte("your-secret-key"), nil
})
if err != nil {
return nil, fmt.Errorf("failed to parse token: %w", err)
}
// Type assert the claims
claims, ok := token.Claims.(*CustomClaims)
if !ok || !token.Valid {
return nil, fmt.Errorf("invalid token claims")
}
// Additional validation
if err := validateClaims(claims); err != nil {
return nil, err
}
return claims, nil
}
func validateClaims(claims *CustomClaims) error {
// Validate expiration
if claims.ExpiresAt.Time.Before(time.Now()) {
return fmt.Errorf("token has expired")
}
// Validate issuer (if needed)
if claims.Issuer != "expected-issuer" {
return fmt.Errorf("invalid issuer")
}
// Validate subject format (example)
if len(claims.Subject) < 10 {
return fmt.Errorf("invalid subject format")
}
// Add any other custom validation logic
return nil
}
Security Best Practices
1. Algorithm Verification
Always verify the signing algorithm to prevent algorithm switching attacks:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
2. Expiration Time
Always check token expiration:
if claims.ExpiresAt.Time.Before(time.Now()) {
return nil, fmt.Errorf("token has expired")
}
3. Secret Key Management
- Use strong, randomly generated secret keys
- Rotate keys periodically
- Store keys securely (e.g., using environment variables or secret management systems)
- Never hardcode keys in your source code
4. Additional Claims Validation
Consider validating:
- Issuer (
iss
) - Audience (
aud
) - Subject (
sub
) - Custom claims specific to your application
Common Pitfalls
Insufficient Validation Don’t just check if the token is valid; validate all relevant claims.
Missing Algorithm Verification Always verify the signing algorithm to prevent downgrade attacks.
Weak Secret Keys Use strong, random secret keys of appropriate length.
Clock Skew Consider allowing a small time buffer for expiration checks:
const clockSkew = 1 * time.Minute
if claims.ExpiresAt.Time.Add(-clockSkew).Before(time.Now()) {
return nil, fmt.Errorf("token has expired")
}
Example Usage
Here’s how to use the verification in an HTTP handler:
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Extract token from Authorization header
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "missing authorization header", http.StatusUnauthorized)
return
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
// Verify token
claims, err := verifyToken(tokenString)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
// Add claims to request context
ctx := context.WithValue(r.Context(), "claims", claims)
next.ServeHTTP(w, r.WithContext(ctx))
}
}
Conclusion
Proper JWT verification is crucial for application security. By following these practices and implementing thorough validation, you can ensure your application handles JWTs securely. Remember to:
- Validate all relevant claims
- Use strong secret keys
- Implement proper error handling
- Consider security implications of token management
Always stay updated with security best practices and be prepared to adapt your implementation as new security considerations emerge.