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:

  1. Header (algorithm and token type)
  2. Payload (claims)
  3. 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 subject
  • exp (Expiration Time): Token expiry timestamp
  • iat (Issued At): Token creation timestamp
  • iss (Issuer): Token issuer
  • aud (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

  1. Insufficient Validation Don’t just check if the token is valid; validate all relevant claims.

  2. Missing Algorithm Verification Always verify the signing algorithm to prevent downgrade attacks.

  3. Weak Secret Keys Use strong, random secret keys of appropriate length.

  4. 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.