Zero-Dependency JWT Decoding in Go
Go's standard library has native Base64URL support (encoding/base64.RawURLEncoding), so you can decode a JWT payload with zero external packages.
package main
import (
"encoding/base64"
"encoding/json"
"errors"
"strings"
)
// DecodePayload decodes a JWT payload without verifying the signature.
// Use for inspection and logging only.
func DecodePayload(token string) (map[string]interface{}, error) {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return nil, errors.New("not a JWT")
}
payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, err
}
var claims map[string]interface{}
if err := json.Unmarshal(payloadBytes, &claims); err != nil {
return nil, err
}
return claims, nil
}
// Usage
func main() {
claims, _ := DecodePayload("eyJhbGciOi...")
// Access claims["sub"], claims["exp"], etc.
}This works in Go 1.x with only standard library. Use for logging, debugging, and developer tools. Never for authorization.
Using golang-jwt/jwt v5 — The Standard Library
The community-maintained fork of dgrijalva/jwt-go. Production-grade, well-documented, actively developed.
Installation
go get github.com/golang-jwt/jwt/v5Verify with HS256 Secret (Map Claims)
package main
import (
"fmt"
"os"
"github.com/golang-jwt/jwt/v5"
)
func verifyToken(tokenString string) (jwt.MapClaims, error) {
token, err := jwt.Parse(tokenString,
func(token *jwt.Token) (interface{}, error) {
// Verify the signing method to prevent alg confusion attacks
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(os.Getenv("JWT_SECRET")), nil
},
jwt.WithIssuer("https://auth.example.com"),
jwt.WithAudience("https://api.example.com"),
jwt.WithExpirationRequired(),
jwt.WithLeeway(30 * time.Second), // Clock drift tolerance
)
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}Verify with Typed Claims (Recommended)
type MyCustomClaims struct {
UserID string `json:"user_id"`
Email string `json:"email"`
Roles []string `json:"roles"`
jwt.RegisteredClaims // embeds sub, iss, aud, exp, iat, nbf, jti
}
claims := &MyCustomClaims{}
token, err := jwt.ParseWithClaims(tokenString, claims,
func(token *jwt.Token) (interface{}, error) {
return []byte(os.Getenv("JWT_SECRET")), nil
},
)
if err != nil || !token.Valid {
return nil, err
}
// Type-safe access — no assertions needed
fmt.Println(claims.UserID, claims.Email, claims.Roles)
fmt.Println(claims.Subject, claims.ExpiresAt.Time)Verify with RS256 and JWKS
// go get github.com/MicahParks/keyfunc/v3
import "github.com/MicahParks/keyfunc/v3"
k, err := keyfunc.NewDefault([]string{
"https://auth.example.com/.well-known/jwks.json",
})
if err != nil {
log.Fatal(err)
}
token, err := jwt.Parse(tokenString, k.Keyfunc)
// keyfunc auto-caches, auto-refreshes on kid mismatch, and is goroutine-safe
if err == nil && token.Valid {
claims := token.Claims.(jwt.MapClaims)
// Use claims
}Gin JWT Middleware
package middleware
import (
"net/http"
"os"
"strings"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
func JwtAuth() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if !strings.HasPrefix(authHeader, "Bearer ") {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Missing token"})
return
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
return []byte(os.Getenv("JWT_SECRET")), nil
})
if err != nil || !token.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
return
}
claims := token.Claims.(jwt.MapClaims)
c.Set("userID", claims["sub"])
c.Set("claims", claims)
c.Next()
}
}
// Usage
r := gin.Default()
authed := r.Group("/api", JwtAuth())
authed.GET("/me", func(c *gin.Context) {
c.JSON(200, gin.H{"userID": c.GetString("userID")})
})Fiber JWT Middleware
// go get github.com/gofiber/contrib/jwt
import (
"github.com/gofiber/fiber/v2"
jwtware "github.com/gofiber/contrib/jwt"
)
app := fiber.New()
app.Use(jwtware.New(jwtware.Config{
SigningKey: jwtware.SigningKey{
JWTAlg: jwtware.HS256,
Key: []byte(os.Getenv("JWT_SECRET")),
},
}))
app.Get("/me", func(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
return c.JSON(fiber.Map{"userID": claims["sub"]})
})Echo JWT Middleware
// go get github.com/labstack/echo-jwt/v4
import (
"github.com/labstack/echo/v4"
echojwt "github.com/labstack/echo-jwt/v4"
)
e := echo.New()
e.Use(echojwt.WithConfig(echojwt.Config{
SigningKey: []byte(os.Getenv("JWT_SECRET")),
}))
e.GET("/me", func(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
return c.JSON(200, map[string]interface{}{"userID": claims["sub"]})
})Common Go JWT Errors
- jwt.ErrTokenExpired — exp is in the past. Client must refresh.
- jwt.ErrTokenNotValidYet — nbf is in the future. Check clock sync.
- jwt.ErrTokenSignatureInvalid — signature does not match your secret/public key.
- jwt.ErrTokenMalformed — token is not three dot-separated segments.
- jwt.ErrTokenInvalidAudience / ErrTokenInvalidIssuer — aud or iss does not match your WithAudience/WithIssuer option.
- "unexpected signing method: none" — an attacker tried to use the "none" algorithm. Your keyFunc should reject any method that is not what you configured.
Security Best Practices for Go JWT
- Always type-check the signing method in your keyFunc:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok. Prevents alg confusion attacks where an attacker downgrades RS256 → HS256 using your public key as the "secret". - Use
jwt.WithExpirationRequired()to reject tokens without exp — never trust a permanent token. - Use typed claims (RegisteredClaims + custom struct) instead of MapClaims — compile-time safety, no runtime type assertions.
- For JWKS, use MicahParks/keyfunc/v3 — it handles caching, rotation, and rate limiting automatically.
- Store the JWT_SECRET in env vars. HS256 secret must be at least 32 random bytes.
- Do not log full tokens. Log only the jti (JWT ID) and sub (subject) for audit trails.
Key Facts
- Standard lib:
- github.com/golang-jwt/jwt/v5 (community fork of dgrijalva)
- JWKS lib:
- github.com/MicahParks/keyfunc/v3 (cached, auto-rotate)
- HS256 secret:
- 32+ random bytes. Store in env, not source.
- Type assertion:
- Always type-check token.Method to prevent alg confusion
- Framework mws:
- gin-contrib/jwt, gofiber/contrib/jwt, labstack/echo-jwt
Related JWT Tools
- JWT Decoder Online — decode any token instantly
- JWT Decoder Node.js — jsonwebtoken and jose patterns
- JWT Decoder Python — PyJWT for Flask/Django/FastAPI
- JWT Decoder Java — jjwt and Spring Boot patterns
- JWT Decoder PHP — firebase/php-jwt and Laravel patterns