Decode a JWT in Go with golang-jwt/jwt
If a Go HTTP middleware rejects a JWT, ParseUnverified can show which claim the server later failed to trust. Go's standard JWT library is github.com/golang-jwt/jwt/v5, the current major version with Go 1.21+ support.1 It provides ParseUnverified for payload inspection without a key, ParseWithClaims for full signature verification, and a typed claims interface for structured access. Add it with go get github.com/golang-jwt/jwt/v5 and import it as jwt in your source file. Paste any token into the decoder above for instant offline inspection without setting up a Go module first.
Pasting tokens into server-backed tools exposes them to third-party servers. Browser-based inspection eliminates this risk entirely with no network calls. For application code, Go's golang-jwt/jwt library uses an idiomatic key function pattern: you provide a func(*jwt.Token) (interface{}, error) that selects the correct verification key based on the token's header claims. This pattern supports multi-provider key selection and dynamic JWKS resolution without coupling your verification logic to a specific issuer.
Library and setup
Install the library with go get github.com/golang-jwt/jwt/v5 from your module root, using the import path github.com/golang-jwt/jwt/v5 with the package name jwt, and note that this v5 major version replaced the legacy github.com/dgrijalva/jwt-go and github.com/form3tech-oss/jwt-go packages that are both unmaintained and contain known security vulnerabilities including CVE-2020-26160.2 jwt.MapClaims provides a map[string]interface{} for dynamic claim access when you need to read arbitrary keys without declaring struct fields.3 Embedding jwt.RegisteredClaims in a struct gives you typed fields for standard claims like Sub, ExpiresAt, and Issuer with compile-time safety that prevents typos in claim-name strings.4
Choosing MapClaims or a claims struct
Use jwt.MapClaims when you are exploring an unfamiliar token and need every key without declaring Go fields first, then migrate to a typed claims struct once the expected claim names are stable. Starting with MapClaims in tests and moving to a struct in production middleware keeps debugging fast without weakening validation, and a typed claims struct with json tags also serves as documentation for which claims your handler expects.
Decoding without verification
Inspecting a JWT without verifying its signature uses jwt.NewParser().ParseUnverified(token, jwt.MapClaims{}), which returns a *jwt.Token with the Claims field populated from the payload and a Valid field set to false since no cryptographic verification occurred.1 Read claims using the MapClaims type where claims["sub"] returns the subject as interface{} that you assert to string with the ok idiom, and this type-assertion pattern is necessary for every claim read from MapClaims because the JSON decoder stores all values as interface{}.3 A simpler debugging approach embeds jwt.RegisteredClaims in a struct and passes it to ParseUnverified, giving you typed fields like claims.Subject and claims.ExpiresAt without the assertion noise.
Reading specific claims
For structured claim access, define a struct embedding jwt.RegisteredClaims and add custom fields, then pass a pointer to this struct as the claims argument to ParseUnverified or ParseWithClaims so the library populates all fields via JSON unmarshalling. Building on this, jwt.RegisteredClaims provides ExpiresAt (a *jwt.NumericDate), IssuedAt, Issuer, Subject, Audience, and ID as typed Go fields, which means you get compile-time checking for the most common claim names while retaining the flexibility to add provider-specific fields alongside them.5
For provider-specific claims, like cognito:groups or realm_access, add them as struct fields with json tags matching the claim name exactly as it appears in the token payload. Field names containing colons require the json:"cognito:groups" tag form; arrays use []string and objects use map[string]interface{}, so printing the raw parsed claims first helps you choose the correct Go type for each nested structure.
Combining golang-jwt with lestrrat-go/jwx for JWKS resolution
For JWKS-based RS256 verification in Go, add lestrrat-go/jwx alongside golang-jwt/jwt, installing with go get github.com/lestrrat-go/jwx/v2 and importing the jwk and jws subpackages for key-set management. Create a cached key set with jwk.NewCachedSet(ctx, jwksURL) then call jws.Verify([]byte(token), jws.WithKeySet(ks)) to verify the signature without managing key rotation manually, because the cached set refreshes automatically when a token arrives with an unknown kid that is not present in the current cache.6
Complete claim validation after signature verification
For complete claim validation after signature verification, parse the verified payload into your claims struct using json.Unmarshal or pass the token to jwt.ParseWithClaims with a key function that resolves from the same jwk cache. This two-library pattern is the standard approach in Go microservices where dynamic JWKS resolution and strict claim validation are both required at the gateway layer, handling all key rotation logic internally so your application code only needs to focus on validating the expected iss, aud, and exp values.
Keeping ParseUnverified out of production routes
Keep ParseUnverified out of production route guards because it reads the payload without verifying the signature or enforcing exp, which means any caller can construct a token with arbitrary claims that your server would accept. Use it only to read the header and kid during debugging, then send the original token through the verified ParseWithClaims path before your handler makes an access decision. A grep for ParseUnverified across your codebase during code review is a quick way to catch accidental usage in route handlers.
Adding this check to your continuous integration pipeline catches accidental introduction of insecure parsing before it reaches production. A single ParseUnverified call in a hot path can become an open door if the surrounding code starts trusting its output. Treat the grep as a guardrail that complements, rather than replaces, the verified parsing you use at request time, and reach for Go JWT parsing without module setup instead when you just need to confirm a claim before writing any code, since it gives you the same view as ParseUnverified minus the module setup.
When to use this
Use golang-jwt/jwt in Go HTTP servers and Lambda functions that receive JWTs from any standard provider. ParseUnverified suits middleware debugging; ParseWithClaims with a key function suits production route guards.
Examples
Inspect JWT payload without verification in Go
import "github.com/golang-jwt/jwt/v5"
token := "eyJ..."
p, _, err := jwt.NewParser().ParseUnverified(
token,
jwt.MapClaims{},
)
if claims, ok := p.Claims.(jwt.MapClaims); ok {
fmt.Println(claims["sub"])
fmt.Println(claims["exp"])
} ParseUnverified never returns an error for a well-formed JWT, even if the signature is wrong or the token is expired.
Typed claims struct for provider-specific fields
type MyClaims struct {
jwt.RegisteredClaims
// Keycloak roles
RealmAccess map[string][]string `json:"realm_access"`
// Cognito groups
Groups []string `json:"cognito:groups"`
}
var claims MyClaims
jwt.NewParser().ParseUnverified(token, &claims)
fmt.Println(claims.Subject)
fmt.Println(claims.Groups) JSON tags must exactly match the claim name, including colons in provider-specific claim names like cognito:groups.
- 1.
golang-jwt, "jwt/v5," pkg.go.dev, accessed June 2026. https://pkg.go.dev/github.com/golang-jwt/jwt/[email protected]
- 2.
NVD, "CVE-2020-26160," nvd.nist.gov, September 2020. https://nvd.nist.gov/vuln/detail/CVE-2020-26160
- 3.
golang-jwt, "map_claims.go," github.com, accessed June 2026. https://github.com/golang-jwt/jwt/blob/main/map_claims.go
- 4.
golang-jwt, "registered_claims.go," github.com, accessed June 2026. https://github.com/golang-jwt/jwt/blob/main/registered_claims.go
- 5.
M. Jones et al., "JSON Web Token (JWT)," RFC 7519, IETF, May 2015. https://datatracker.ietf.org/doc/html/rfc7519
- 6.
lestrrat-go, "jwx v2 — jwk.Cache," pkg.go.dev, accessed June 2026. https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jwk#Cache