Guide 2 min read Updated

Verify JWT in Go (golang-jwt & go-jose)

Verify JWT signatures in Go with golang-jwt and go-jose. Sign and verify HS256 and RS256 tokens, validate iss/aud/exp, and verify JWTs against a JWKS endpoint with full Go code examples.

Go has two production-grade JWT libraries: golang-jwt/jwt/v5 (the maintained successor to the deprecated dgrijalva/jwt-go) and go-jose/go-jose/v4 (the JOSE-spec library used when you need JWKS, EdDSA, or JWE).

Install

go get github.com/golang-jwt/jwt/v5
go get github.com/go-jose/go-jose/v4

Sign and verify an HS256 token (golang-jwt)

package main

import (
	"crypto/rand"
	"github.com/golang-jwt/jwt/v5"
	"time"
)

var secret = []byte(os.Getenv("JWT_SECRET")) // 32+ bytes

func signToken() (string, error) {
	claims := jwt.MapClaims{
		"sub": "user_123",
		"iss": "https://auth.example.com",
		"aud": "https://api.example.com",
		"exp": time.Now().Add(15 * time.Minute).Unix(),
		"iat": time.Now().Unix(),
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(secret)
}

func verifyToken(tokenString string) (jwt.MapClaims, error) {
	token, err := jwt.ParseWithClaims(tokenString, jwt.MapClaims{}, func(t *jwt.Token) (interface{}, error) {
		if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected method: %v", t.Header["alg"])
		}
		return secret, nil
	},
		jwt.WithValidMethods([]string{"HS256"}),      // mandatory
		jwt.WithIssuer("https://auth.example.com"),
		jwt.WithAudience("https://api.example.com"),
		jwt.WithLeeway(60*time.Second),               // skew tolerance
	)
	if err != nil {
		return nil, err
	}
	return token.Claims.(jwt.MapClaims), nil
}

Verify a JWT against a JWKS endpoint (go-jose + keyfunc)

import (
	"github.com/go-jose/go-jose/v4"
	"github.com/MicahParks/keyfunc/v3"
	"github.com/golang-jwt/jwt/v5"
)

// keyfunc fetches and caches the JWKS, refreshing on a TTL
jwks, err := keyfunc.New(keyfunc.Options{
	JWKSURL: "https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json",
	RefreshErrorHandler: func(err error) {
		log.Printf("JWKS refresh error: %v", err)
	},
})
if err != nil {
	log.Fatal(err)
}

token, err := jwt.ParseWithClaims(tokenString, jwt.MapClaims{}, jwks.Keyfunc,
	jwt.WithValidMethods([]string{"RS256"}),
	jwt.WithIssuer("https://YOUR_DOMAIN.auth0.com/"),
	jwt.WithAudience("https://api.example.com"),
)

keyfunc handles the JWKS fetch, caching, and background refresh. Do not fetch the JWKS on every request — the latency and the issuer-down dependency are both unacceptable in production.

Sign and verify an RS256 token (go-jose)

signer, _ := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: privateKey}, &jose.SignerOptions{
	ExtraHeaders: map[jose.HeaderKey]interface{}{"kid": "rsa-key-v1"},
})
jws, _ := signer.Sign([]byte(`{"sub":"user_123","iss":"https://auth.example.com","aud":"https://api.example.com","exp":` + strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10) + `}`))
token := jws.CompactSerialize()

// Verify
object, _ := jose.ParseSigned(token)
payload, err := object.Verify(publicKey)   // also validates the algorithm
if err != nil {
	log.Fatal("signature invalid")
}

go-jose’s Verify rejects algorithms not matched to the key type, which closes the algorithm-confusion attack at the library level.


Continue reading