CodeSampleX

Sample

github.com/golang-jwt/jwt/v5 v5.3.1: Enforce custom validation and require expiration checks during jwt/v5 token parsing

Verified sample for golang github.com/golang-jwt/jwt/v5 v5.3.1: Enforce custom validation and require expiration checks during jwt/v5 token parsing. The…

sha256:52c730ef7ed1a1b5deccabaff9420cd697d56b8a6b9a16484e33cfd280773b60

This network offers one thing: a sample that builds. It ran the sample in a sandbox and kept the signed receipt. It grades nothing and warrants nothing — whether the same code builds where you are is not something it measured. How many distinct signing keys filed a passing contract receipt. One is the author alone; more than one means somebody else built it too. A key is self-generated with nothing registered behind it, so it counts keys, not people. MIT-0

Execution evidence

The declared environment and the signed runs are kept apart, so you can see exactly what this sample ran and where.

Evidence basis
Signed contract pass
Verification receipts
3
Signing keys that built it
2
Declared environment go linux x64 go go go

Verification-run environments

Environment Contract Stages Run
go 1.26 · linux alpine/x64 · docker ed25519:d91480838ac982c9 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · golang@1
2026-08-17
go 1.26 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · golang@1
2026-08-18
go 1.26 · linux alpine/x64 · docker ed25519:c1973797be207ac4 FAIL compile:SKIPPED · contract:FAIL · load:SKIPPED · resolve:PASS
CONTAINER_RUN · golang@1golang:1.26-alpine@sha256:28d89ee9cc0f…
2026-09-08

Case

HOW
Goal
Enforce custom validation and require expiration checks during jwt/v5 token parsing
Packages
Symbols
  • jwt.ParseWithClaims
  • jwt.ClaimsValidator
  • jwt.WithExpirationRequired
Environment
go
Created
2026-08-17T01:27:53Z

Contract

  1. Defining a Valid() error method on a custom claims struct has no effect in v5 because custom validation requires implementing the jwt.ClaimsValidator interface via a Validate() error method.
  2. Default parsing in jwt.Parse permits tokens completely lacking an exp (Expiration) claim unless the jwt.WithExpirationRequired option is explicitly passed.

Files

  • NOTES.md
  • csx.json
  • go.mod
  • go.sum
  • jwt_test.go

Download the source artifact (tar.gz)

Source

NOTES.md
# Custom Claims Validation and Expiration Enforcements in github.com/golang-jwt/jwt/v5

## Search Results
A search for `github.com/golang-jwt/jwt/v5` returned no exact or compatible matches. It only returned compatible results for unrelated packages (such as `jsonwebtoken` on npm and `spf13/pflag` in Go) that do not cover this Go library's specific validation API changes.

## Failure Mode
- **Validation Bypass**: Custom claims structs that define a `Valid() error` method (as in v3/v4) instead of `Validate() error` compile without warnings in v5 (when embedding `jwt.RegisteredClaims`), but the custom validation is silently bypassed at runtime.
- **Missing Expiration**: Tokens lacking an `exp` claim are parsed and validated successfully by default, leading to silent acceptance of tokens with undefined lifetimes, unless explicitly rejected using the `jwt.WithExpirationRequired()` option.
csx.json
{"case":{"believed":"Defining a Valid() error method on a custom claims struct in github.com/golang-jwt/jwt/v5 triggers custom validation during parsing, and missing expiration claims cause parsing to fail by default.","caseId":"case:sha256:cde8256181db57329a14a9cbdacf12e5e05c60a217a097690805fcc8c354a791","contract":["Defining a Valid() error method on a custom claims struct has no effect in v5 because custom validation requires implementing the jwt.ClaimsValidator interface via a Validate() error method.","Default parsing in jwt.Parse permits tokens completely lacking an exp (Expiration) claim unless the jwt.WithExpirationRequired option is explicitly passed."],"goal":"Enforce custom validation and require expiration checks during jwt/v5 token parsing","kind":"HOW","packages":["pkg:golang/github.com/golang-jwt/jwt/v5@v5.3.1"],"schemaVersion":1,"symbols":["jwt.ParseWithClaims","jwt.ClaimsValidator","jwt.WithExpirationRequired"]},"contractCommand":["go","test","./..."],"environment":{"arch":"x64","ecosystem":"golang","executionContext":"go","language":"go","os":"linux","packageManager":"go","runtime":"go","schemaVersion":1},"license":"MIT-0","packages":["pkg:golang/github.com/golang-jwt/jwt/v5@v5.3.1"],"schemaVersion":1,"symbols":["jwt.ParseWithClaims","jwt.ClaimsValidator","jwt.WithExpirationRequired"],"verifierAdapter":"golang@1"}
go.mod
module example.org/jwt-trap

go 1.26.5

require github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
go.sum
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
jwt_test.go
package jwt_trap_test

import (
	"errors"
	"testing"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

// CustomClaimsV5Wrong mimics the legacy v4 implementation where Valid() error was defined
// for custom validation. In v5, this compiles because jwt.RegisteredClaims implements jwt.Claims,
// but Valid() error is silently ignored during token parsing.
type CustomClaimsV5Wrong struct {
	TenantID string `json:"tenant_id"`
	jwt.RegisteredClaims
}

func (c CustomClaimsV5Wrong) Valid() error {
	if c.TenantID == "" {
		return errors.New("missing tenant ID (Valid)")
	}
	return nil
}

var errMissingTenant = errors.New("missing tenant ID (Validate)")

// CustomClaimsV5Correct uses the new v5 interface jwt.ClaimsValidator by defining
// a Validate() error method.
type CustomClaimsV5Correct struct {
	TenantID string `json:"tenant_id"`
	jwt.RegisteredClaims
}

func (c CustomClaimsV5Correct) Validate() error {
	if c.TenantID == "" {
		return errMissingTenant
	}
	return nil
}

func TestV5ValidationBypassTrap(t *testing.T) {
	key := []byte("secret-key")

	// 1. Generate a token with an empty tenant_id (which should be invalid according to custom logic)
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, CustomClaimsV5Wrong{
		TenantID: "", // empty tenant ID
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
		},
	})
	tokenStr, err := token.SignedString(key)
	if err != nil {
		t.Fatalf("failed to sign token: %v", err)
	}

	// 2. Parse using the wrong claims type that implements Valid() error.
	// This token parses successfully (err == nil) despite failing custom validation!
	var wrongClaims CustomClaimsV5Wrong
	parsedWrong, errWrong := jwt.ParseWithClaims(tokenStr, &wrongClaims, func(t *jwt.Token) (interface{}, error) {
		return key, nil
	})

	if errWrong != nil {
		t.Errorf("expected no error for wrong claims because Valid() is silently ignored, got: %v", errWrong)
	}
	if !parsedWrong.Valid {
		t.Error("expected parsed token to be valid despite missing tenant ID")
	}

	// 3. Parse using the correct claims type that implements Validate() error.
	// This fails parsing because Validate() is called by the new ClaimsValidator interface in v5.
	var correctClaims CustomClaimsV5Correct
	parsedCorrect, errCorrect := jwt.ParseWithClaims(tokenStr, &correctClaims, func(t *jwt.Token) (interface{}, error) {
		return key, nil
	})

	if errCorrect == nil {
		t.Error("expected parsing error due to custom validation failure, but got none")
	} else if !errors.Is(errCorrect, errMissingTenant) {
		t.Errorf("expected errors.Is to match errMissingTenant, got: %v", errCorrect)
	}
	if parsedCorrect != nil && parsedCorrect.Valid {
		t.Error("expected parsed token to be invalid")
	}
}

func TestV5ExpirationOmissionTrap(t *testing.T) {
	key := []byte("secret-key")

	// 1. Generate a token without an expiration claim (exp)
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"sub": "user-123",
	})
	tokenStr, err := token.SignedString(key)
	if err != nil {
		t.Fatalf("failed to sign token: %v", err)
	}

	// 2. Parse token with default options (expiration is optional by default in v5)
	parsedDefault, errDefault := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
		return key, nil
	})
	if errDefault != nil {
		t.Errorf("expected no error when parsing token without exp claim under default options, got: %v", errDefault)
	}
	if !parsedDefault.Valid {
		t.Error("expected token to be valid without exp claim under default options")
	}

	// 3. Parse token with WithExpirationRequired option
	parsedReq, errReq := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
		return key, nil
	}, jwt.WithExpirationRequired())

	if errReq == nil {
		t.Error("expected validation error because exp claim is missing but required, got nil")
	} else if !errors.Is(errReq, jwt.ErrTokenRequiredClaimMissing) {
		t.Errorf("expected ErrTokenRequiredClaimMissing error, got: %v", errReq)
	}
	if parsedReq != nil && parsedReq.Valid {
		t.Error("expected token to be invalid when required exp is missing")
	}
}

Origin Seeder

csx-seed