zerojwt

package module
v1.3.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 21, 2026 License: MIT Imports: 20 Imported by: 0

README ΒΆ

⚑ zerojwt β€” Ultra-Fast, Zero-Allocation Industrial JWT for Go

English | PortuguΓͺs

Go Reference Go Report Card Coverage Allocations

zerojwt is a pure Go, zero-dependency library for signing and verifying JSON Web Tokens (JWT / RFC 7519) in ~215 ns/op (parallel) / ~415–700 ns/op (single-core) with guaranteed 0 B/op and 0 allocs/op on critical execution paths.

Born out of PR #48 (go-gnet-kaikegm) in The $12 Server Challenge β€” where a single OS thread sustained 300,000 concurrent keep-alive connections and 35,577 req/s β€” zerojwt eliminates the reflection, encoding/json dynamic map allocations, and Garbage Collector (GC) pressure of traditional Go JWT packages while delivering complete, industrial-grade capabilities.

[!TIP] Looking for zerojwt v2? Check out github.com/KaikeGM/zerojwt/v2 featuring unified 1016-byte OIDC claims, secure-by-default constructor, atomic claim iteration (iter.Seq2), and event-driven network primitives (VerifySplit, WorkerScratchGroup, SignToIOV). See also the runnable examples/gnet gateway!


πŸ“‘ Table of Contents


πŸš€ Benchmark Comparison

All benchmarks are 100% reproducible and published directly in this repository under the benchmarks/ directory. The benchmark suite tests each library with the exact same cryptographic key, token payload, claims schema, and verification workload.

Note on Zero Dependencies: The competitor benchmarks are housed in a separate Go module (benchmarks/go.mod). This guarantees that your production code importing github.com/KaikeGM/zerojwt remains strictly zero third-party dependencies.

πŸ”¬ Reproducing the Benchmarks

Clone the repository and run the benchmark suite directly:

cd benchmarks
go test -bench=. -benchmem -benchtime=1s

All four libraries are verified for identical claim extraction and cryptographic correctness before the benchmark runs via TestCompetitorsCompatibility.

πŸ“Š Benchmark Results

Environment: Intel(R) Core(TM) i5-1135G7 @ 2.40GHz (8 logical CPUs), Linux amd64, Go 1.23+. Payload: HS256 32-byte secret, standard registered claims (sub: "12345", iat, exp), plus custom claim (user: "golden_ember_1"). Fair clock evaluation: Verification benchmarks for zerojwt invoke time.Now().Unix() on each iteration to match competing libraries that call system time internally.

1. Token Verification (Verify & Parse Claims)
Library Mode / Strategy Throughput (ops/s = 1e9/ns) Avg Time / op Memory / op Allocs / op
zerojwt Parallel (Multi-Core) ~4,750,000 ops/s 210.5 ns/op 0 B/op 0 allocs/op
zerojwt Single-Core (Scratch) ~1,387,000 ops/s 720.9 ns/op 0 B/op 0 allocs/op
zerojwt Single-Core (Pool) ~1,372,000 ops/s 728.5 ns/op 0 B/op 0 allocs/op
kataras/jwt Parallel (Multi-Core) ~951,000 ops/s 1,051.0 ns/op 1,075 B/op 15 allocs/op
kataras/jwt Single-Core ~354,000 ops/s 2,821.0 ns/op 1,073 B/op 15 allocs/op
golang-jwt/jwt/v5 Parallel (Multi-Core) ~575,000 ops/s 1,739.0 ns/op 2,097 B/op 37 allocs/op
golang-jwt/jwt/v5 Single-Core ~209,000 ops/s 4,780.0 ns/op 2,091 B/op 37 allocs/op
lestrrat-go/jwx/v2 Parallel (Multi-Core) ~185,000 ops/s 5,385.0 ns/op 6,135 B/op 108 allocs/op
lestrrat-go/jwx/v2 Single-Core ~91,000 ops/s 10,948.0 ns/op 6,112 B/op 108 allocs/op
2. Token Generation (Signing)
Library Implementation Throughput (ops/s = 1e9/ns) Avg Time / op Memory / op Allocs / op
zerojwt SignClaims ~2,300,000 ops/s 434.7 ns/op 0 B/op 0 allocs/op
zerojwt SignBuilder ~2,083,000 ops/s 480.0 ns/op 0 B/op 0 allocs/op
kataras/jwt jwt.Sign ~619,000 ops/s 1,613.0 ns/op 1,208 B/op 14 allocs/op
golang-jwt/jwt/v5 SignedString ~276,000 ops/s 3,617.0 ns/op 2,338 B/op 36 allocs/op
lestrrat-go/jwx/v2 jwt.Sign ~131,000 ops/s 7,617.0 ns/op 4,933 B/op 118 allocs/op

Key takeaway: Traditional JWT libraries allocate between 14 and 108 objects on the heap per token validation, accumulating megabytes of garbage and triggering constant GC pauses under production load (10k+ req/s). zerojwt performs zero heap allocations, allowing web gateways and event loops to handle maximum connection density with flat, predictable latencies.


✨ Key Features

  1. Strict 0 Allocations Guarantee:

    • Token signing and verification with HS256/HS384/HS512 and EdDSA operate without heap allocations (0 B/op, 0 allocs/op). ES256 still allocates inside crypto/ecdsa (varies with Go runtime version: ~8–10 allocs/op verify, ~33–67 allocs/op sign).
    • No interface{} unboxing, no map[string]any parsing, no reflection (reflect).
  2. Complete Cryptographic Algorithm Portfolio:

    • Symmetric: HS256, HS384, HS512 (HMAC with SHA-2 family).
    • Asymmetric: EdDSA (Ed25519 / RFC 8037) and ES256 (ECDSA P-256 / RFC 7518).
    • Custom ASN.1 DER to IEEE P1363 fixed-width (64-byte $r \parallel s$) conversion implemented directly on stack buffers.
  3. Strict RFC 7519 / RFC 7515 Compliance:

    • Configurable clock skew tolerance (Leeway).
    • Standard claims: exp (Expires At), nbf (Not Before), iat (Issued At), iss (Issuer), aud (Audience), sub (Subject), and jti (JWT ID).
    • Audience matching supports both single JSON strings ("aud":"service") and JSON string arrays ("aud":["svc-a","svc-b"]) in zero allocations.
    • Enforces typ: "JWT" validation if present in the header.
  4. Zero-Copy In-Place Lexer (scanner.go):

    • High-performance, streaming JSON tokenizer with precomputed 256-byte Lookup Tables (LUTs) for whitespace, delimiters, and hex conversion.
    • In-place string unescaping for standard escapes (\", \\, \/, \b, \f, \n, \r, \t) and full Unicode code points (\uXXXX), including UTF-16 surrogate pairs (\uD800..\uDBFF + \uDC00..\uDFFF).
  5. Multi-Key Credential Rotation (kid):

    • Seamless key rotation with zero allocations using the "kid" header parameter.
  6. Stack-Allocated Fluent SignBuilder:

    • Construct and sign custom payloads with arbitrary fields without allocating JSON strings or maps.
  7. Fail-Fast Configuration & Best-Effort Memory Hygiene:

    • Constructors reject unusable key material (empty HMAC secrets, keys without material for the selected algorithm, wrong key sizes or curves) instead of building a verifier that accepts forged tokens.
    • Optional ZeroMemory mode wipes claims and scratch buffers via memclr before returning them to the pool. This is memory hygiene, not a compliance claim.
  8. Zero Third-Party Dependencies:

    • Built exclusively with the Go standard library (crypto/*, encoding/base64, sync, unsafe).

πŸ›οΈ Architecture & Memory Model

zerojwt achieves zero heap allocations by replacing dynamic runtime allocations with reusable Scratch workspaces:

+-------------------------------------------------------------------------+
|                               Scratch                                   |
|                                                                         |
|  +--------------------+  +--------------------+  +-------------------+  |
|  |   hdrBuf [128]B    |  |  payloadBuf [1024]B|  |   sigBuf [64]B    |  |
|  +--------------------+  +--------------------+  +-------------------+  |
|  +--------------------+  +--------------------+  +-------------------+  |
|  |   sumBuf [64]B     |  |   derBuf [72]B     |  |  mac (hash.Hash)  |  |
|  +--------------------+  +--------------------+  +-------------------+  |
+-------------------------------------------------------------------------+
Dual Concurrency Models
  1. Multithreaded Web Applications (sync.Pool): Methods like Verifier.Verify, Verifier.VerifyLarge, Verifier.VerifyBearer, and Verifier.Sign automatically check out a *Scratch instance from an internal lock-free sync.Pool and return it upon completion.
  2. Single-Threaded Event Loops (gnet / epoll / Worker Pools): Methods like Verifier.VerifyWithScratch, Verifier.VerifyLargeWithScratch, and Verifier.SignWithScratch accept a caller-managed *Scratch directly (e.g. pinned to the event loop's worker context), eliminating even the atomic overhead of sync.Pool.

πŸ“¦ Installation

Requires Go 1.23 or newer:

go get github.com/KaikeGM/zerojwt

πŸ› οΈ Quick Start

For production services, prefer NewSecureVerifier. It automatically enforces modern identity standards (OIDC Core 1.0 Β§3.1.3.7, OAuth 2.0 RFC 9068 Β§2.2, and RFC 7518 Β§3.2):

  • Mandatory Expiration: Rejects tokens without an exp claim (ErrMissingExp).
  • Cryptographic Key Strength: Enforces minimum HMAC key size ($\ge 32$ bytes for HS256).
  • Error Handling: Returns (*Verifier, error) at initialization rather than panicking.
package main

import (
	"fmt"
	"time"

	"github.com/KaikeGM/zerojwt"
)

func main() {
	secret := []byte("secret-key-must-be-at-least-32-bytes!!")
	verifier, err := zerojwt.NewSecureVerifier(secret)
	if err != nil {
		panic(err)
	}
	now := time.Now().Unix()

	// 1. Sign claims with typed error handling (0 allocs/op)
	var tokBuf [512]byte
	token, err := verifier.TrySignClaims(
		tokBuf[:0],
		[]byte("user_9876"), // sub (up to 256 bytes)
		[]byte("kaikegm"),   // username
		now,                 // iat
		now+3600,            // exp (1 hour)
	)
	if err != nil {
		panic(err)
	}

	fmt.Printf("Generated JWT: %s\n", token)

	// 2. Verify token into stack-allocated Claims (0 allocs/op)
	var claims zerojwt.Claims
	if err := verifier.Verify(token, now, &claims); err != nil {
		panic(err)
	}

	fmt.Printf("Authenticated: Subject=%s, Username=%s, ExpiresAt=%d\n",
		claims.Subject(), claims.Username(), claims.ExpiresAt)
}
2. Baseline RFC 7519: NewVerifier

If your architecture requires tokens without an expiration timestamp or uses legacy keys, NewVerifier preserves baseline RFC 7519 defaults (RequireExp: false, permissive key size):

verifier := zerojwt.NewVerifier(secret)
token := verifier.SignClaims(tokBuf[:0], []byte("user_1"), nil, now, now+3600)

[!TIP] API Ergonomics & Error Returns:

  • Typed Error Path (TrySign*): TrySign and TrySignClaims return ([]byte, error) with explicit sentinel errors (ErrPayloadTooLarge, ErrMissingExp, ErrNoSigningKey, ErrSigningFailed), preserving 0 B/op, 0 allocs/op.
  • Direct Slice Path (Sign*): Sign and SignClaims return []byte directly (and nil on failure) for maximum speed in event-loop environments.
  • HMAC Key Lengths: StrictSecretLength applies to symmetric HMAC algorithms (HS256, HS384, HS512 per RFC 7518 Β§3.2). Asymmetric keys (EdDSA, ES256) have mathematically fixed curve dimensions enforced directly at initialization.

πŸ” Cryptographic Algorithms

1. Symmetric HMAC: HS256, HS384, HS512

Symmetric validation requires the same shared secret between issuer and validator.

// HS256 (Default)
v256 := zerojwt.NewVerifier(secret)

// HS384
v384 := zerojwt.NewVerifierWithOptions(secret, zerojwt.Options{
	Algorithm: zerojwt.AlgHS384,
})

// HS512
v512 := zerojwt.NewVerifierWithOptions(secret, zerojwt.Options{
	Algorithm: zerojwt.AlgHS512,
})

2. Asymmetric: EdDSA (Ed25519 / RFC 8037)

Ed25519 provides ultra-fast asymmetric verification with 32-byte public keys and 64-byte signatures. Ideal for microservices architectures where internal services should only verify tokens without having the capability to issue them.

import "crypto/ed25519"

// Generate keypair
pubKey, privKey, _ := ed25519.GenerateKey(nil)

// Authentication Server (Issues & Signs tokens)
authVerifier := zerojwt.NewEd25519Verifier(pubKey, privKey, zerojwt.Options{
	ExpectedIssuer: "https://auth.company.com",
})
token := authVerifier.SignClaims(tokBuf[:0], []byte("usr_1"), []byte("alice"), now, now+3600)

// Microservice / API Gateway (Verify-only: privKey is nil)
gatewayVerifier := zerojwt.NewEd25519Verifier(pubKey, nil, zerojwt.Options{
	ExpectedIssuer: "https://auth.company.com",
})
var claims zerojwt.Claims
if err := gatewayVerifier.Verify(token, now, &claims); err != nil {
	log.Fatalf("Invalid token: %v", err)
}

3. Asymmetric: ES256 (ECDSA P-256 / RFC 7518)

ES256 is widely used in enterprise OAuth 2.0 / OpenID Connect, mobile clients, and WebAuthn. zerojwt implements direct IEEE P1363 ($r \parallel s$) to ASN.1 DER conversion on stack buffers. Note that crypto/ecdsa itself allocates, so ES256 is not zero-allocation end to end.

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
)

// Generate P-256 keypair
privKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
pubKey := &privKey.PublicKey

// Verifier with ES256
vES256 := zerojwt.NewES256Verifier(pubKey, privKey, zerojwt.Options{
	Leeway: 10 * time.Second,
})

token := vES256.Sign(tokBuf[:0], []byte(`{"sub":"usr_p256","exp":1893456000}`))

πŸ“‹ RFC 7519 Validation & Claims

Clock Skew Tolerance (Leeway)

In distributed systems, servers may experience slight NTP clock desynchronization. The Leeway option prevents premature token rejection:

verifier := zerojwt.NewVerifierWithOptions(secret, zerojwt.Options{
	Leeway: 5 * time.Second, // Allows tokens up to 5s before nbf or 5s after exp
})

Issuer (iss) & Audience (aud) Validation

When configured, zerojwt enforces that incoming tokens contain matching iss and aud claims:

verifier := zerojwt.NewVerifierWithOptions(secret, zerojwt.Options{
	ExpectedIssuer:   "auth.enterprise.io",
	ExpectedAudience: "payments-api",
})

zerojwt natively validates aud claims declared as either a single JSON string or a JSON array of strings:

  • Single: "aud": "payments-api"
  • Array: "aud": ["reporting-api", "payments-api", "admin-api"]

Both variations are parsed and matched in-place with 0 heap allocations.


JWT ID (jti) & Replay Prevention

The jti claim provides a unique nonce (e.g. UUID v4) to prevent replay attacks. Claims exposes claims.JTI() to check against in-memory revocation lists (such as a Bloom filter or synchronized cache):

var claims zerojwt.Claims
if err := verifier.Verify(token, now, &claims); err != nil {
	return err
}

if revocationFilter.Contains(claims.JTI()) {
	return errors.New("token has been revoked")
}

Dual Claims Structs: Claims vs ClaimsLarge
Feature Claims (Default) ClaimsLarge
Target Use Case High-density APIs, microservices Enterprise tokens, long usernames/UUIDs
Max Subject Length 64 bytes 256 bytes
Max Username Length 128 bytes 256 bytes
Max Issuer Length 64 bytes 256 bytes
Max Audience Length 64 bytes 256 bytes
Max JTI Length 36 bytes (UUID) 64 bytes
Memory Footprint Extremely compact Slightly larger fixed stack size
Verification Method v.Verify(...) v.VerifyLarge(...)

πŸ› οΈ Custom Claims & Fluent SignBuilder

Fluent SignBuilder (0 allocs/op)

Build custom JWT payloads with arbitrary attributes without allocating map[string]any or strings:

var tokBuf [1024]byte
builder := verifier.NewSigner(tokBuf[:0])
defer builder.Discard() // Safely returns Scratch to pool if an error returns before Sign()

token, err := builder.
	SetSubject([]byte("usr_1001")).
	SetUsername([]byte("kaike")).
	SetIssuer([]byte("auth.company.com")).
	SetAudience([]byte("api.company.com")).
	SetJTI([]byte("9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d")).
	SetIssuedAt(now).
	SetExpiresAt(now + 7200).
	SetClaimString("role", "admin").
	SetClaimInt("tenant_id", 42).
	SetClaimBool("mfa_verified", true).
	SetClaimRawJSON("permissions", []byte(`["read:users","write:users"]`)).
	Sign()

if err != nil {
	panic(err)
}

Memory safety tip: Sign() automatically returns the pooled *Scratch instance on completion. Adding defer builder.Discard() protects against memory leaks when an error or early return occurs before Sign() is invoked; calling Discard() after Sign() is a safe no-op.


Arbitrary Claims Inspection via VerifyCustom

If your token contains arbitrary nested structures or custom attributes, use VerifyCustom. It validates signature and temporal claims (exp, nbf, iss, aud), and yields key-value slices directly from the verified buffer with 0 allocations:

err := verifier.VerifyCustom(token, now, func(key, val []byte, isStr bool) bool {
	switch string(key) {
	case "tenant_id":
		id, _ := zerojwt.ParsePositiveInt(val)
		fmt.Printf("Tenant: %d\n", id)
	case "permissions":
		// Go 1.23+ range-over-func iterator (0 B/op, 0 allocs/op, ~54 ns/op):
		for elem, isStr := range zerojwt.ArrayElements(val) {
			fmt.Printf("Permission: %s (isStr=%v)\n", elem, isStr)
		}
	}
	return true // continue scanning claims
})
Go 1.23+ Zero-Allocation Range-Over-Func Iterators

Go 1.23 standardizes iterators via iter.Seq and iter.Seq2. zerojwt provides native iterators that compile down with complete closure inlining, running at ~18,000,000–19,000,000 ops/s (~52–55 ns/op) with 0 B/op and 0 allocs/op:

// 1. Iterate over JSON array elements (non-destructive, safe for multiple passes):
for elem, isStr := range zerojwt.ArrayElements(rawArray) {
	fmt.Printf("Element: %s (isStr: %v)\n", elem, isStr)
}

// 2. Iterate over JSON object properties (key, val):
for key, val := range zerojwt.ObjectProps(rawObject) {
	fmt.Printf("%s = %s\n", key, val)
}

// 3. Iterate over properties with full type information:
for prop := range zerojwt.ObjectProperties(rawObject) {
	fmt.Printf("Key: %s, Val: %s, IsString: %v\n", prop.Key, prop.Val, prop.IsStr)
}

String values note: String elements and property values are yielded as non-destructive slices into the underlying buffer with enclosing quotes stripped, retaining escape sequences (e.g., \", \n) to preserve buffer immutability across multiple passes. Call zerojwt.UnescapeJSONInPlace(val) if decoded strings are needed.

Strict Error Detection

When iterating over untrusted or external JSON payloads, iterators accept an optional *error pointer. If the JSON structure is truncated or encounters invalid syntax, iteration stops gracefully and assigns zerojwt.ErrInvalidPayload:

var scanErr error
for elem, isStr := range zerojwt.ArrayElements(rawArray, &scanErr) {
	// ...
}
if scanErr != nil {
	return fmt.Errorf("malformed claims array: %w", scanErr)
}

πŸ”„ Zero-Downtime Key Rotation (kid)

To rotate keys without invalidating existing sessions, register multiple keys with unique IDs:

verifier := zerojwt.NewVerifierWithOptions(primarySecret, zerojwt.Options{
	Algorithm:   zerojwt.AlgHS256,
	ActiveKeyID: "key-2026-v2", // Embedded into new tokens
	Keys: []zerojwt.Key{
		{ID: "key-2025-v1", Secret: oldSecret},
		{ID: "key-2026-v2", Secret: primarySecret},
	},
})

Every key must carry material for the verifier's algorithm: Secret for HS256/384/512, a 32-byte Ed25519Public for EdDSA, a P-256 ECDSAPublic for ES256. IDs must be non-empty and unique. A key that does not match the algorithm is a configuration error (see Security Hardening); a verifier handles one algorithm, so keys of different algorithms cannot be mixed in one Verifier.

When validating, zerojwt extracts the "kid" claim from the header and uses the corresponding cryptographic key in $O(1)$ without heap allocations.


🌐 High-Speed HTTP Middlewares

1. ProtectFunc (Fastest, Zero-Allocation)

Passes a stack-allocated *Claims pointer directly to your handler, ensuring 0 B/op overhead per HTTP request:

http.HandleFunc("/api/me", verifier.ProtectFunc(func(w http.ResponseWriter, r *http.Request, claims *zerojwt.Claims) {
	w.Header().Set("Content-Type", "application/json")
	fmt.Fprintf(w, `{"sub":"%s","user":"%s"}`, claims.Subject(), claims.Username())
}))

2. Standard net/http & Chi Middleware

Middleware stores the claims in the request context, which allocates. Use ProtectFunc when you need zero allocations per request.

Integrates seamlessly with Chi, Gorilla Mux, or standard Go routers:

r := chi.NewRouter()
r.Use(verifier.Middleware)

r.Get("/api/dashboard", func(w http.ResponseWriter, r *http.Request) {
	claims, ok := zerojwt.ClaimsFromContext(r.Context())
	if !ok {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}
	fmt.Fprintf(w, "Welcome, user ID %d!", claims.SubjectID)
})

3. Gin, Fiber & gnet Integration
Gin
func ZeroJWTGin(v *zerojwt.Verifier) gin.HandlerFunc {
	return func(c *gin.Context) {
		auth := c.GetHeader("Authorization")
		if len(auth) <= 7 {
			c.AbortWithStatus(http.StatusUnauthorized)
			return
		}
		var claims zerojwt.Claims
		if err := v.VerifyBearer([]byte(auth), time.Now().Unix(), &claims); err != nil {
			c.AbortWithStatus(http.StatusUnauthorized)
			return
		}
		c.Set("claims", &claims)
		c.Next()
	}
}
gnet (Single-Thread Event Loop)
type server struct {
	*gnet.BuiltinEventEngine
	verifier *zerojwt.Verifier
	scratch  *zerojwt.Scratch // 1 scratch per worker thread
}

func (s *server) OnTraffic(c gnet.Conn) gnet.Action {
	buf, _ := c.Next(-1)
	var claims zerojwt.Claims
	// Direct scratch verification: 0 allocations, 0 sync.Pool locks
	if err := s.verifier.VerifyWithScratch(buf, time.Now().Unix(), &claims, s.scratch); err != nil {
		c.Write([]byte("HTTP/1.1 401 Unauthorized\r\n\r\n"))
		return gnet.Close
	}
	// ...
	return gnet.None
}

πŸ›‘οΈ Security Hardening & Memory Hygiene

Fail-Fast Configuration

NewVerifier, NewVerifierWithOptions, NewEd25519Verifier and NewES256Verifier panic with an error wrapping zerojwt.ErrInvalidConfig when the configuration is unusable, so a missing JWT_SECRET crashes at startup instead of silently accepting forged tokens. To handle the error yourself, use TryNewVerifier:

v, err := zerojwt.TryNewVerifier([]byte(os.Getenv("JWT_SECRET")), zerojwt.Options{
	Algorithm:          zerojwt.AlgHS256,
	RequireExp:         true, // reject tokens without "exp"; SignClaims/SignBuilder refuse to omit it
	StrictSecretLength: true, // enforce the RFC 7518 Β§3.2 minimum: 32/48/64 bytes for HS256/384/512
})
if err != nil {
	log.Fatal(err) // errors.Is(err, zerojwt.ErrInvalidConfig)
}

Always rejected: empty HMAC secrets, Keys without material for the selected algorithm, empty or duplicate key IDs, unknown algorithm names, wrong Ed25519 key sizes and non-P-256 ECDSA keys. StrictSecretLength is opt-in so existing deployments with shorter (but non-empty) secrets keep working.

Strict Token Form and Headers

A token has exactly one accepted string form: Base64URL is decoded in strict mode (non-canonical trailing bits are rejected) and any \r or \n in the token is rejected. This matters if you key a revocation list or replay cache on the token string. A crit header is rejected, as RFC 7515 Β§4.1.11 requires for unsupported critical extensions.

Verify-Only Nodes

Verifier.CanSign() reports whether the verifier holds a signing key. On verify-only nodes (for example EdDSA/ES256 without a private key) the Sign* methods return nil and SignBuilder.Sign returns ErrNoSigningKey; they never return a token with an invalid signature.

Memory Hygiene (ZeroMemory, Destroy)

Enable ZeroMemory: true to memclr claims and scratch buffers before they go back to the pool:

verifier := zerojwt.NewVerifierWithOptions(secret, zerojwt.Options{
	ZeroMemory: true,
})

verifier.Destroy() wipes the secrets the verifier owns and makes it unusable (Verify* returns ErrVerifierDestroyed, Sign* returns nil). This is best-effort hygiene, not a FIPS / PCI-DSS compliance claim: copies of keys you passed in, HMAC state cached inside pooled Scratch values (the standard library's hmac keeps key-derived pads) and an *ecdsa.PrivateKey you share with the Verifier (only the reference is dropped). Destroy must not be called concurrently with other methods.


⚑ Profile-Guided Optimization (PGO)

When compiling production binaries, Go 1.20+ supports Profile-Guided Optimization (PGO). The Go compiler automatically detects and uses a default.pgo profile file located directly in the main package directory being built.

Because Go applies PGO at the executable level rather than per-dependency, you can optimize your entire service (including zerojwt's inner verification and parsing routines) by collecting a profile from production:

# 1. Capture a representative 30-second CPU profile from your running service
curl -o default.pgo "http://localhost:6060/debug/pprof/profile?seconds=30"

# 2. Place default.pgo in your main package directory (alongside main.go)

# 3. Compile with Go 1.23+ (the compiler automatically discovers default.pgo)
go build -o my-service .

To verify PGO optimizations during the build:

go build -gcflags="-m=2" 2>&1 | grep "pgo inline"

πŸ“– API Reference

Verifier Constructors
  • NewVerifier(secret []byte) *Verifier: Creates an HS256 verifier with default buffer limits (128B header, 1024B payload).
  • NewEd25519Verifier(pub ed25519.PublicKey, priv ed25519.PrivateKey, opts Options) *Verifier: Creates an EdDSA (Ed25519) verifier.
  • NewES256Verifier(pub *ecdsa.PublicKey, priv *ecdsa.PrivateKey, opts Options) *Verifier: Creates an ES256 (ECDSA P-256) verifier.
  • NewVerifierWithOptions(secret []byte, opts Options) *Verifier: Creates a fully configured verifier. Panics (wrapping ErrInvalidConfig) on unusable configuration.
  • TryNewVerifier(secret []byte, opts Options) (*Verifier, error): Same, but returns the error instead of panicking.
  • (*Verifier).CanSign() bool: Reports whether the verifier holds a signing key.
Verification Methods
  • Verify(tok []byte, nowUnix int64, out *Claims) error: Validates token into standard Claims.
  • VerifyBearer(authHeader []byte, nowUnix int64, out *Claims) error: Extracts Bearer <token> and validates.
  • VerifyWithScratch(tok []byte, nowUnix int64, out *Claims, st *Scratch) error: Validates token using a caller-supplied Scratch.
  • VerifyLarge(tok []byte, nowUnix int64, out *ClaimsLarge) error: Validates token into large-capacity ClaimsLarge.
  • VerifyLargeWithScratch(tok []byte, nowUnix int64, out *ClaimsLarge, st *Scratch) error: Validates token into ClaimsLarge using caller-supplied Scratch.
  • VerifyCustom(tok []byte, nowUnix int64, onClaim func(key, val []byte, isStr bool) bool) error: Validates signature and temporal claims, scanning arbitrary claims with zero allocations.
Signing Methods & Builder

All Sign* methods return nil (never a token with a bogus signature) when the verifier cannot sign (CanSign() == false), when the signing operation fails, or, with RequireExp, when SignClaims is called with exp <= 0.

  • Sign(dst []byte, payloadJSON []byte) []byte: Signs raw JSON payload using default buffers.
  • SignClaims(dst, sub, username []byte, iat, exp int64) []byte: Signs standard claims directly into dst.
  • SignWithScratch(dst, payloadJSON []byte, st *Scratch) []byte: Signs payload using caller-supplied Scratch.
  • NewSigner(dst []byte) SignBuilder: Creates a fluent builder for custom claims backed by sync.Pool.
  • NewSignerWithScratch(dst []byte, st *Scratch) SignBuilder: Creates a fluent builder using caller-supplied Scratch.
  • (*SignBuilder).Discard(): Releases internal Scratch buffer back to sync.Pool if owned (idempotent).
  • (*SignBuilder).Sign() ([]byte, error): Signs the payload and automatically releases pooled Scratch.
Fast Lexer & Parser (scanner.go)
  • NextJSONProp(b []byte) (key, val []byte, isStr bool, rest []byte, ok bool): Extracts the next "key": value from a JSON object body in-place.
  • NextArrayElement(b []byte) (val []byte, isStr bool, rest []byte, ok bool): Extracts the next element from a JSON array body in-place.
  • TrimJSONObject(b []byte) ([]byte, bool): Strips whitespace and outer { and } braces.
  • TrimJSONArray(b []byte) ([]byte, bool): Strips whitespace and outer [ and ] brackets.
  • ParseNumeric(b []byte) (float64, bool): Parses integer, decimal, and scientific notation floats in zero allocations.
  • ParsePositiveInt(b []byte) (int64, bool): Ultra-fast 64-bit integer parser supporting up to 19 digits (Snowflake IDs).
  • UnescapeJSONInPlace(s []byte) []byte: Decodes escape sequences and Unicode code points in-place.
  • AppendEscapedJSONString(dst, s []byte) []byte: Appends a JSON-safe quoted string.
Go 1.23+ Iterators (iter.go)
  • ArrayElements(rawArray []byte, errOut ...*error) iter.Seq2[[]byte, bool]: Non-destructive traversal of JSON array elements with zero allocations.
  • ObjectProps(rawObject []byte, errOut ...*error) iter.Seq2[[]byte, []byte]: Non-destructive traversal of JSON object key-value pairs with zero allocations.
  • ObjectProperties(rawObject []byte, errOut ...*error) iter.Seq[JSONProp]: Non-destructive traversal of JSON object properties with full type metadata.

⚠️ Sentinel Errors

All errors are pre-allocated static sentinels. Compare using errors.Is or == with zero heap allocations:

Sentinel Error Description
ErrMissingBearer The Authorization header is missing the "Bearer " prefix.
ErrInvalidToken Malformed token structure (does not have exactly 3 dot-separated Base64URL segments).
ErrInvalidHeader Header is not valid JSON, or specifies an unsupported/mismatched algorithm.
ErrInvalidSignature Cryptographic signature verification failed.
ErrExpiredToken Current time is $\ge$ exp claim (taking Leeway into account).
ErrNotValidYet Current time is $<$ nbf claim (taking Leeway into account).
ErrInvalidPayload Payload JSON is malformed, missing required claims (sub), or types mismatch.
ErrPayloadTooLarge Payload length exceeds MaxPayloadSize.
ErrInvalidIssuer The iss claim does not match ExpectedIssuer.
ErrInvalidAudience The aud claim does not match or contain ExpectedAudience.
ErrUnknownKeyID The "kid" header specifies an unrecognized key ID.
ErrMissingExp RequireExp is set and the token has no exp claim.
ErrInvalidConfig Wrapped by TryNewVerifier errors and constructor panics for unusable configuration.
ErrNoSigningKey SignBuilder.Sign was called on a verifier that cannot sign.
ErrVerifierDestroyed A Verify* method was called after Destroy().

πŸ§ͺ Testing, Fuzzing & Quality Assurance

zerojwt features a 100% comprehensive test and validation suite:

# 1. Run all unit tests with 100% statement coverage
go test -v -coverprofile=coverage.out ./...
go tool cover -func=coverage.out

# 2. Race detector (detects concurrency hazards)
go test -v -race ./...

# 3. Verify zero heap allocations on hot paths
go test -v -run=TestZeroAllocations ./...

# 4. Run benchmarks and memory allocation stats
go test -bench=. -benchmem ./...

# 5. Continuous native fuzz testing for adversarial inputs
go test -fuzz=FuzzVerify -fuzztime=30s

πŸ“„ License

MIT License β€” Copyright (c) 2026 KaikeGM. Distributed under the permissive MIT license. See LICENSE for full details.

Documentation ΒΆ

Overview ΒΆ

Package zerojwt provides an ultra-fast, zero-allocation (0 B/op, 0 allocs/op) JWT validator and signer in pure Go supporting HS256, HS384, HS512, EdDSA (Ed25519), and ES256.

Deprecated: Use github.com/KaikeGM/zerojwt/v2 instead.

Index ΒΆ

Examples ΒΆ

Constants ΒΆ

View Source
const (
	// MaxHeaderSize is the default maximum decoded byte length of a JWT header in zero-alloc mode.
	MaxHeaderSize = 128
	// MaxPayloadSize is the default maximum decoded byte length of a JWT payload in zero-alloc mode.
	MaxPayloadSize = 1024
)

Variables ΒΆ

View Source
var (
	// ErrMissingBearer is returned by VerifyBearer when the "Bearer " prefix is absent.
	ErrMissingBearer = errors.New("zerojwt: missing Bearer prefix")

	// ErrInvalidToken is returned when the token does not have 3 valid Base64URL segments.
	ErrInvalidToken = errors.New("zerojwt: malformed token structure")

	// ErrInvalidHeader is returned when the JWT header is not valid JSON or specifies an unsupported algorithm.
	ErrInvalidHeader = errors.New("zerojwt: invalid header or unsupported algorithm")

	// ErrInvalidSignature is returned when the cryptographic signature verification fails.
	ErrInvalidSignature = errors.New("zerojwt: signature verification failed")

	// ErrExpiredToken is returned when the current timestamp is >= the "exp" claim.
	ErrExpiredToken = errors.New("zerojwt: token is expired")

	// ErrNotValidYet is returned when the current timestamp is < the "nbf" claim.
	ErrNotValidYet = errors.New("zerojwt: token is not valid yet (nbf)")

	// ErrInvalidPayload is returned when the payload JSON is malformed or required claims are invalid.
	ErrInvalidPayload = errors.New("zerojwt: invalid token payload claims")

	// ErrPayloadTooLarge is returned when the decoded payload exceeds the configured MaxPayloadSize.
	ErrPayloadTooLarge = errors.New("zerojwt: payload exceeds maximum zero-alloc buffer size")

	// ErrInvalidIssuer is returned when the "iss" claim is missing or does not match ExpectedIssuer.
	ErrInvalidIssuer = errors.New("zerojwt: invalid or mismatched token issuer (iss)")

	// ErrInvalidAudience is returned when the "aud" claim is missing or does not match ExpectedAudience.
	ErrInvalidAudience = errors.New("zerojwt: invalid or mismatched token audience (aud)")

	// ErrUnknownKeyID is returned when the header specifies a "kid" that is not registered in the Verifier.
	ErrUnknownKeyID = errors.New("zerojwt: unknown key ID (kid) in header")

	// ErrMissingExp is returned when Options.RequireExp is set and the token has no "exp" claim.
	ErrMissingExp = errors.New("zerojwt: token has no exp claim")

	// ErrInvalidConfig is wrapped by the errors TryNewVerifier returns (and the panics of the
	// New* constructors) for unusable configurations such as an empty HMAC secret.
	ErrInvalidConfig = errors.New("zerojwt: invalid configuration")

	// ErrNoSigningKey is returned by SignBuilder.Sign when the Verifier cannot sign (see Verifier.CanSign).
	ErrNoSigningKey = errors.New("zerojwt: verifier has no signing key")

	// ErrVerifierDestroyed is returned by Verify* after Verifier.Destroy has been called.
	ErrVerifierDestroyed = errors.New("zerojwt: verifier has been destroyed")

	// ErrSigningFailed is returned by TrySign* when cryptographic signing fails
	// (such as an invalid private key scalar or signing primitive failure).
	ErrSigningFailed = errors.New("zerojwt: cryptographic signing failed")
)

Pre-allocated static errors returned by zerojwt. Comparing against these errors with errors.Is or == performs zero allocations.

Functions ΒΆ

func AppendEscapedJSONString ΒΆ

func AppendEscapedJSONString(dst, s []byte) []byte

AppendEscapedJSONString appends a JSON-escaped string (with surrounding quotes) to dst using unrolled 8-byte vectorized span checks and bulk memory copies.

func ArrayElements ΒΆ added in v1.2.0

func ArrayElements(rawArray []byte, errOut ...*error) iter.Seq2[[]byte, bool]

ArrayElements returns an iter.Seq2[[]byte, bool] (element, isString) that traverses a raw JSON array (e.g., `["admin", "user"]`) in-place with guaranteed 0 B/op and 0 allocs/op. String elements are yielded as slices into the underlying buffer (without outer quotes); they remain in their raw escaped form to preserve buffer immutability across multiple passes. Call UnescapeJSONInPlace(elem) if unescaped content is needed. An optional errOut pointer can be passed to capture any JSON syntax error or truncation; if omitted or if nil, iteration terminates without assigning an error.

func NextArrayElement ΒΆ

func NextArrayElement(b []byte) (val []byte, isStr bool, rest []byte, ok bool)

NextArrayElement extracts the next element from a JSON array body (inside '[' and ']'). It operates strictly in-place on b with zero heap allocations (0 B/op, 0 allocs/op). String elements have their enclosing quotes stripped and are returned as non-destructive slices into b, preserving any escape sequences so that b is not modified and can be traversed multiple times safely. Use UnescapeJSONInPlace(val) if unescaped content is needed.

func NextJSONProp ΒΆ

func NextJSONProp(b []byte) (key, val []byte, isStr bool, rest []byte, ok bool)

NextJSONProp extracts the next top-level "key": value pair from a JSON object body (inside '{' and '}'). It operates strictly in-place on b with zero heap allocations (0 B/op, 0 allocs/op). String values have their enclosing quotes stripped and are returned as non-destructive slices into b, preserving any escape sequences (e.g. \" or \u00e7) so that b is not modified and can be traversed multiple times. Use UnescapeJSONInPlace(val) if unescaped content is needed.

func ObjectProperties ΒΆ added in v1.2.0

func ObjectProperties(rawObject []byte, errOut ...*error) iter.Seq[JSONProp]

ObjectProperties returns an iter.Seq[JSONProp] that traverses a raw JSON object in-place, yielding key, value, and isString type information with 0 B/op and 0 allocs/op. An optional errOut pointer can be passed to capture any JSON syntax error or truncation.

func ObjectProps ΒΆ added in v1.2.0

func ObjectProps(rawObject []byte, errOut ...*error) iter.Seq2[[]byte, []byte]

ObjectProps returns an iter.Seq2[[]byte, []byte] (key, val) that traverses a raw JSON object (e.g., `{"role": "admin", "tenant": "acme"}`) in-place with guaranteed 0 B/op and 0 allocs/op. Values remain in their raw escaped form to preserve buffer immutability across multiple passes. Call UnescapeJSONInPlace(val) if unescaped content is needed. An optional errOut pointer can be passed to capture any JSON syntax error or truncation.

func ParseNumeric ΒΆ

func ParseNumeric(b []byte) (float64, bool)

ParseNumeric parses a JSON number (integer or floating point, e.g. Unix timestamps) without allocations.

func ParsePositiveInt ΒΆ

func ParsePositiveInt(val []byte) (int64, bool)

ParsePositiveInt parses a strictly positive 64-bit integer from a byte slice of digits. Supports full int64 range up to 19 digits (e.g. distributed snowflake IDs).

func TrimJSONArray ΒΆ

func TrimJSONArray(b []byte) ([]byte, bool)

TrimJSONArray strips surrounding whitespace and outer '[' and ']' brackets from a JSON array.

func TrimJSONObject ΒΆ

func TrimJSONObject(b []byte) ([]byte, bool)

TrimJSONObject strips surrounding whitespace and outer '{' and '}' braces from a JSON object.

func UnescapeJSONInPlace ΒΆ

func UnescapeJSONInPlace(s []byte) []byte

UnescapeJSONInPlace unescapes JSON string escape sequences in-place without allocating memory.

Types ΒΆ

type Algorithm ΒΆ

type Algorithm string

Algorithm represents a supported JWS cryptographic algorithm (RFC 7518 / RFC 8037).

const (
	AlgHS256 Algorithm = "HS256"
	AlgHS384 Algorithm = "HS384"
	AlgHS512 Algorithm = "HS512"
	AlgEdDSA Algorithm = "EdDSA"
	AlgES256 Algorithm = "ES256"
)

type Claims ΒΆ

type Claims struct {
	SubjectID   int64 // Parsed positive integer ID when "sub" is numeric (e.g. "123" -> 123)
	IssuedAt    int64 // "iat" claim (Unix seconds)
	ExpiresAt   int64 // "exp" claim (Unix seconds)
	NotBefore   int64 // "nbf" claim (Unix seconds)
	HasSub      bool  // True if "sub" claim was present
	HasUsername bool  // True if "username" claim was present
	HasIss      bool  // True if "iss" claim was present
	HasAud      bool  // True if "aud" claim was present
	HasJti      bool  // True if "jti" claim was present
	HasExp      bool  // True if "exp" claim was present
	HasNbf      bool  // True if "nbf" claim was present
	HasIat      bool  // True if "iat" claim was present
	// contains filtered or unexported fields
}

Claims holds standard JWT claims on the caller's stack without heap allocations. Its internal fixed buffers own the copied claim bytes, ensuring 100% safety against data races even after the internal Scratch buffer returns to the pool.

func ClaimsFromContext ΒΆ

func ClaimsFromContext(ctx context.Context) (*Claims, bool)

ClaimsFromContext retrieves the verified *Claims from a request context populated by Verifier.Middleware.

func (*Claims) Audience ΒΆ

func (c *Claims) Audience() []byte

Audience returns the primary "aud" claim as a byte slice backed by Claims' inline buffer.

func (*Claims) Clear ΒΆ

func (c *Claims) Clear()

Clear resets all fields and explicitly zeroes all internal claim buffers with 0x00 (memclr).

func (*Claims) Issuer ΒΆ

func (c *Claims) Issuer() []byte

Issuer returns the "iss" claim as a byte slice backed by Claims' inline buffer.

func (*Claims) JTI ΒΆ

func (c *Claims) JTI() []byte

JTI returns the "jti" (JWT ID) claim as a byte slice backed by Claims' inline buffer.

func (*Claims) Reset ΒΆ

func (c *Claims) Reset()

Reset resets length markers and flags for fast reuse without zeroing the underlying byte arrays.

func (*Claims) Subject ΒΆ

func (c *Claims) Subject() []byte

Subject returns the "sub" claim as a byte slice backed by Claims' inline buffer.

func (*Claims) Username ΒΆ

func (c *Claims) Username() []byte

Username returns the "username" claim as a byte slice backed by Claims' inline buffer.

type ClaimsLarge ΒΆ

type ClaimsLarge struct {
	SubjectID   int64
	IssuedAt    int64
	ExpiresAt   int64
	NotBefore   int64
	HasSub      bool
	HasUsername bool
	HasIss      bool
	HasAud      bool
	HasJti      bool
	HasExp      bool
	HasNbf      bool
	HasIat      bool
	// contains filtered or unexported fields
}

ClaimsLarge provides expanded 256-byte inline buffers for deployments where Subject, Username, Issuer, Audience, or JTI identifiers exceed 64/128 bytes.

func (*ClaimsLarge) Audience ΒΆ

func (c *ClaimsLarge) Audience() []byte

func (*ClaimsLarge) Clear ΒΆ

func (c *ClaimsLarge) Clear()

func (*ClaimsLarge) Issuer ΒΆ

func (c *ClaimsLarge) Issuer() []byte

func (*ClaimsLarge) JTI ΒΆ

func (c *ClaimsLarge) JTI() []byte

func (*ClaimsLarge) Reset ΒΆ

func (c *ClaimsLarge) Reset()

func (*ClaimsLarge) Subject ΒΆ

func (c *ClaimsLarge) Subject() []byte

func (*ClaimsLarge) Username ΒΆ

func (c *ClaimsLarge) Username() []byte

type JSONProp ΒΆ added in v1.2.0

type JSONProp struct {
	Key   []byte
	Val   []byte
	IsStr bool
}

JSONProp represents a key-value property scanned in-place from a JSON object.

type Key ΒΆ

type Key struct {
	ID            string
	Secret        []byte
	Ed25519Public ed25519.PublicKey
	ECDSAPublic   *ecdsa.PublicKey
}

Key represents a named cryptographic key for "kid" (Key ID) rotation in JWT headers.

type Options ΒΆ

type Options struct {
	// Algorithm selects the JWS algorithm (default: AlgHS256).
	Algorithm Algorithm
	// MaxHeaderSize sets the maximum decoded byte length of a JWT header (default: 128).
	MaxHeaderSize int
	// MaxPayloadSize sets the maximum decoded byte length of a JWT payload (default: 1024).
	MaxPayloadSize int
	// Leeway defines the clock skew tolerance for "exp" and "nbf" validation.
	Leeway time.Duration
	// ExpectedIssuer enforces that the token's "iss" claim matches this value.
	ExpectedIssuer string
	// ExpectedAudience enforces that the token's "aud" claim (string or array) contains this value.
	ExpectedAudience string
	// Keys registers named keys for automatic "kid" header lookup during verification.
	Keys []Key
	// ActiveKeyID embeds `"kid":"<ActiveKeyID>"` in the JWT header when signing tokens.
	ActiveKeyID string
	// Ed25519PublicKey is the 32-byte public key used when Algorithm is AlgEdDSA.
	Ed25519PublicKey ed25519.PublicKey
	// Ed25519PrivateKey is the 64-byte private key used for signing when Algorithm is AlgEdDSA.
	Ed25519PrivateKey ed25519.PrivateKey
	// ECDSAPublicKey is the P-256 public key used when Algorithm is AlgES256.
	ECDSAPublicKey *ecdsa.PublicKey
	// ECDSAPrivateKey is the P-256 private key used for signing when Algorithm is AlgES256.
	ECDSAPrivateKey *ecdsa.PrivateKey
	// ZeroMemory zeroes out sensitive scratch buffers with 0x00 (memclr) after each operation.
	// This is best-effort memory hygiene, not a compliance guarantee.
	ZeroMemory bool
	// RequireExp makes verification fail with ErrMissingExp when a token carries no "exp"
	// claim, and makes SignClaims refuse to sign with exp <= 0. Default: false.
	RequireExp bool
	// StrictSecretLength enforces the RFC 7518 Β§3.2 minimum HMAC key size
	// (32 bytes for HS256, 48 for HS384, 64 for HS512). Empty keys are always rejected,
	// regardless of this option. Default: false.
	StrictSecretLength bool
}

Options configures algorithm, buffer dimensions, RFC 7519 validation rules, key rotation ("kid"), and FIPS/PCI-DSS memory sanitization for a Verifier.

func SecureOptions ΒΆ added in v1.3.0

func SecureOptions() Options

SecureOptions returns Options preconfigured with strict production security settings aligning with OIDC Core 1.0 Β§3.1.3.7, OAuth 2.0 RFC 9068 Β§2.2, and RFC 7518 Β§3.2:

  • RequireExp: true (rejects tokens without "exp" claim)
  • StrictSecretLength: true (enforces minimum HMAC key sizes: 32B for HS256, 48B for HS384, 64B for HS512)

Note: StrictSecretLength applies to symmetric HMAC algorithms; asymmetric algorithms (EdDSA, ES256) have curve-mandated key sizes enforced directly by mathematical checks.

type Scratch ΒΆ

type Scratch struct {
	// contains filtered or unexported fields
}

Scratch holds reusable buffers and pre-initialized HMAC hashers. In multi-goroutine servers, Verifier manages a sync.Pool of Scratch instances automatically. In single-threaded event loops (such as gnet with LockOSThread), callers can hold a single Scratch instance and call VerifyWithScratch to bypass even sync.Pool overhead.

func (*Scratch) Clear ΒΆ

func (s *Scratch) Clear()

Clear zeroes out all internal working buffers in Scratch using runtime memclr.

type SignBuilder ΒΆ

type SignBuilder struct {
	// contains filtered or unexported fields
}

SignBuilder is a stack-allocated, zero-allocation fluent builder for constructing and signing JWT tokens with arbitrary custom claims.

func (*SignBuilder) Discard ΒΆ added in v1.2.0

func (b *SignBuilder) Discard()

Discard releases the internal Scratch buffer back to the pool if owned, invalidating the builder. It is safe to call Discard multiple times or after Sign. Typical usage:

b := v.NewSigner(dst)
defer b.Discard()

func (*SignBuilder) SetAudience ΒΆ

func (b *SignBuilder) SetAudience(aud []byte) *SignBuilder

SetAudience appends the standard "aud" claim.

func (*SignBuilder) SetClaimBool ΒΆ

func (b *SignBuilder) SetClaimBool(key string, val bool) *SignBuilder

SetClaimBool appends a custom boolean claim.

func (*SignBuilder) SetClaimBytes ΒΆ

func (b *SignBuilder) SetClaimBytes(key string, val []byte) *SignBuilder

SetClaimBytes appends a custom string claim from a byte slice with JSON escaping.

func (*SignBuilder) SetClaimInt ΒΆ

func (b *SignBuilder) SetClaimInt(key string, val int64) *SignBuilder

SetClaimInt appends a custom 64-bit integer claim.

func (*SignBuilder) SetClaimRawJSON ΒΆ

func (b *SignBuilder) SetClaimRawJSON(key string, rawJSON []byte) *SignBuilder

SetClaimRawJSON appends a pre-formatted JSON value (such as an array `["admin","editor"]` or object). The value is copied verbatim and NOT validated or escaped: never pass untrusted input, or the resulting payload may be malformed or contain injected claims.

func (*SignBuilder) SetClaimString ΒΆ

func (b *SignBuilder) SetClaimString(key, val string) *SignBuilder

SetClaimString appends a custom string claim with JSON escaping.

func (*SignBuilder) SetExpiresAt ΒΆ

func (b *SignBuilder) SetExpiresAt(exp int64) *SignBuilder

SetExpiresAt appends the standard "exp" claim (Unix seconds).

func (*SignBuilder) SetIssuedAt ΒΆ

func (b *SignBuilder) SetIssuedAt(iat int64) *SignBuilder

SetIssuedAt appends the standard "iat" claim (Unix seconds).

func (*SignBuilder) SetIssuer ΒΆ

func (b *SignBuilder) SetIssuer(iss []byte) *SignBuilder

SetIssuer appends the standard "iss" claim.

func (*SignBuilder) SetJTI ΒΆ

func (b *SignBuilder) SetJTI(jti []byte) *SignBuilder

SetJTI appends the standard "jti" (JWT ID) claim.

func (*SignBuilder) SetNotBefore ΒΆ

func (b *SignBuilder) SetNotBefore(nbf int64) *SignBuilder

SetNotBefore appends the standard "nbf" claim (Unix seconds).

func (*SignBuilder) SetSubject ΒΆ

func (b *SignBuilder) SetSubject(sub []byte) *SignBuilder

SetSubject appends the standard "sub" claim.

func (*SignBuilder) SetUsername ΒΆ

func (b *SignBuilder) SetUsername(username []byte) *SignBuilder

SetUsername appends the "username" claim.

func (*SignBuilder) Sign ΒΆ

func (b *SignBuilder) Sign() ([]byte, error)

Sign finalizes the JSON payload, signs the token into dst, and returns the resulting slice. If the SignBuilder was created via NewSigner, the internal Scratch is returned to the pool.

type Verifier ΒΆ

type Verifier struct {
	// contains filtered or unexported fields
}

Verifier validates and signs JWT tokens with zero heap allocations (0 B/op, 0 allocs/op). A Verifier is 100% safe for concurrent use by multiple goroutines.

func NewES256Verifier ΒΆ

func NewES256Verifier(pub *ecdsa.PublicKey, priv *ecdsa.PrivateKey, opts Options) *Verifier

NewES256Verifier creates a new concurrent-safe ECDSA P-256 (ES256) Verifier. For verification-only nodes, priv can be nil.

func NewEd25519Verifier ΒΆ

func NewEd25519Verifier(pub ed25519.PublicKey, priv ed25519.PrivateKey, opts Options) *Verifier

NewEd25519Verifier creates a new concurrent-safe EdDSA (Ed25519) Verifier. For verification-only nodes, priv can be nil.

Example ΒΆ

ExampleNewEd25519Verifier demonstrates an asymmetric microservices architecture where the authentication authority signs with an Ed25519 private key, and consuming microservices verify tokens using only the 32-byte public key (privKey = nil).

package main

import (
	"crypto/ed25519"
	"fmt"

	"github.com/KaikeGM/zerojwt"
)

func main() {
	// In production, keys are loaded from secure config/vault.
	// We generate an Ed25519 keypair for demonstration:
	pubKey, privKey, err := ed25519.GenerateKey(nil)
	if err != nil {
		fmt.Printf("Key generation error: %v\n", err)
		return
	}

	now := int64(1700000000)

	// 1. Authentication Server: initialized with both pubKey and privKey to sign tokens.
	authServer := zerojwt.NewEd25519Verifier(pubKey, privKey, zerojwt.Options{
		ExpectedIssuer: "auth.internal",
	})

	var tokBuf [512]byte
	token := authServer.SignClaims(tokBuf[:0], []byte("srv_99"), []byte("payment-worker"), now, now+1800)

	// 2. Client Microservice: initialized with public key ONLY (privKey is nil).
	// This ensures the microservice can verify tokens, but cannot forge or issue them.
	microserviceVerifier := zerojwt.NewEd25519Verifier(pubKey, nil, zerojwt.Options{
		ExpectedIssuer: "auth.internal",
	})

	var claims zerojwt.Claims
	if err := microserviceVerifier.Verify(token, now, &claims); err != nil {
		fmt.Printf("Microservice verification failed: %v\n", err)
		return
	}

	fmt.Printf("Microservice verified token for %s (issuer valid: %t)\n", claims.Username(), claims.HasIss)

}
Output:
Microservice verified token for payment-worker (issuer valid: true)

func NewSecureVerifier ΒΆ added in v1.3.0

func NewSecureVerifier(secret []byte) (*Verifier, error)

NewSecureVerifier creates a new concurrent-safe Verifier preconfigured for production profiles (OIDC Core 1.0 Β§3.1.3.7, RFC 9068 Β§2.2, RFC 7518 Β§3.2). It enforces RequireExp: true and StrictSecretLength: true (minimum 32 bytes for HS256). Returns an error wrapping ErrInvalidConfig if the secret is too short or empty.

func NewSecureVerifierWithOptions ΒΆ added in v1.3.0

func NewSecureVerifierWithOptions(secret []byte, opts Options) (*Verifier, error)

NewSecureVerifierWithOptions creates a new concurrent-safe Verifier applying RequireExp and StrictSecretLength over the caller-provided Options.

func NewVerifier ΒΆ

func NewVerifier(secret []byte) *Verifier

NewVerifier creates a new concurrent-safe HS256 Verifier for the given secret key using the default buffer limits (128B header, 1024B payload).

Note: NewVerifier preserves RFC 7519 baseline defaults (RequireExp: false, StrictSecretLength: false). For production environments, prefer NewSecureVerifier.

func NewVerifierWithOptions ΒΆ

func NewVerifierWithOptions(secret []byte, opts Options) *Verifier

NewVerifierWithOptions creates a new concurrent-safe Verifier with custom algorithm, buffer dimensions, clock skew leeway, issuer/audience rules, and key rotation.

It panics with an error wrapping ErrInvalidConfig when the configuration is unusable (empty HMAC secret, a Key without material for the selected algorithm, unknown algorithm, wrong key size or curve, ...), so that misconfiguration is caught at startup instead of producing a verifier that accepts forged tokens. Use TryNewVerifier to get an error instead.

func TryNewVerifier ΒΆ added in v1.1.0

func TryNewVerifier(secret []byte, opts Options) (*Verifier, error)

TryNewVerifier is like NewVerifierWithOptions but returns an error wrapping ErrInvalidConfig instead of panicking. opts.Algorithm selects the algorithm; for EdDSA and ES256 the keys come from the corresponding Options fields and secret is ignored.

func (*Verifier) CanSign ΒΆ added in v1.1.0

func (v *Verifier) CanSign() bool

CanSign reports whether this Verifier holds the key material needed to issue tokens. Verify-only nodes (for example EdDSA or ES256 without a private key) return false, and Sign* methods return nil on them instead of producing a token with an invalid signature.

func (*Verifier) Destroy ΒΆ

func (v *Verifier) Destroy()

Destroy makes the Verifier unusable and wipes, on a best-effort basis, the key material it owns: the HMAC secret, its private copy of the Ed25519 private key and the secrets of registered Keys. Afterwards Verify* returns ErrVerifierDestroyed and Sign* returns nil.

This is memory hygiene, not a compliance guarantee. Not scrubbed: the slices you passed in, HMAC state cached inside pooled Scratch values (the standard library's hmac keeps key-derived pads) and an *ecdsa.PrivateKey you share with the Verifier (only the reference is dropped). Destroy must not be called concurrently with other methods.

func (*Verifier) Middleware ΒΆ

func (v *Verifier) Middleware(next http.Handler) http.Handler

Middleware provides a standard net/http and chi compatible middleware that validates the Authorization: Bearer <token> header and attaches *Claims to the request's context.Context.

func (*Verifier) NewScratch ΒΆ

func (v *Verifier) NewScratch() *Scratch

NewScratch allocates a new Scratch state bound to this Verifier's keys and buffer sizes. Useful for single-threaded event-loop servers (e.g. gnet) using VerifyWithScratch.

func (*Verifier) NewSigner ΒΆ

func (v *Verifier) NewSigner(dst []byte) SignBuilder

NewSigner creates a fluent SignBuilder backed by the Verifier's Scratch pool. Calling Sign() automatically releases the internal Scratch back to the pool.

Example ΒΆ

ExampleVerifier_NewSigner demonstrates using the fluent SignBuilder to compose and sign a JWT with custom claims entirely on the stack with 0 heap allocations.

package main

import (
	"fmt"

	"github.com/KaikeGM/zerojwt"
)

func main() {
	secret := []byte("super-secret-key-32-bytes-long!!")
	verifier := zerojwt.NewVerifier(secret)
	now := int64(1700000000)

	var tokBuf [512]byte
	b := verifier.NewSigner(tokBuf[:0])
	b.SetSubject([]byte("user_456")).
		SetUsername([]byte("bob")).
		SetIssuer([]byte("auth.service")).
		SetAudience([]byte("api.service")).
		SetClaimString("role", "admin").
		SetClaimInt("tenant_id", 42).
		SetClaimBool("mfa", true).
		SetExpiresAt(now + 3600)

	token, err := b.Sign()
	if err != nil {
		fmt.Printf("Sign error: %v\n", err)
		return
	}

	var claims zerojwt.Claims
	if err := verifier.Verify(token, now, &claims); err != nil {
		fmt.Printf("Verify error: %v\n", err)
		return
	}

	fmt.Printf("Subject: %s\n", claims.Subject())
	fmt.Printf("Username: %s\n", claims.Username())
	fmt.Printf("HasExp: %t\n", claims.HasExp)

}
Output:
Subject: user_456
Username: bob
HasExp: true

func (*Verifier) NewSignerWithScratch ΒΆ

func (v *Verifier) NewSignerWithScratch(dst []byte, st *Scratch) SignBuilder

NewSignerWithScratch creates a fluent SignBuilder using a caller-supplied Scratch (ideal for single-threaded event loops like gnet).

func (*Verifier) ProtectFunc ΒΆ

func (v *Verifier) ProtectFunc(next func(w http.ResponseWriter, r *http.Request, claims *Claims)) http.HandlerFunc

ProtectFunc wraps a handler that receives stack-allocated *Claims directly, achieving 0 B/op and 0 allocs/op per authenticated HTTP request.

Example ΒΆ

ExampleVerifier_ProtectFunc demonstrates wrapping an HTTP handler with ultra-low latency and 0 heap allocations per authenticated request.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"

	"github.com/KaikeGM/zerojwt"
)

func main() {
	secret := []byte("super-secret-key-32-bytes-long!!")
	verifier := zerojwt.NewVerifier(secret)
	now := time.Now().Unix()

	// Create an authenticated HTTP handler receiving stack-allocated *Claims
	handler := verifier.ProtectFunc(func(w http.ResponseWriter, r *http.Request, claims *zerojwt.Claims) {
		fmt.Fprintf(w, "Hello, %s!", claims.Username())
	})

	// Sign a test token
	var tokBuf [512]byte
	token := verifier.SignClaims(tokBuf[:0], []byte("u1"), []byte("charlie"), now, now+3600)

	// Simulate an HTTP request with the Bearer token
	req := httptest.NewRequest("GET", "/profile", nil)
	req.Header.Set("Authorization", "Bearer "+string(token))
	rec := httptest.NewRecorder()

	handler.ServeHTTP(rec, req)

	fmt.Printf("Status: %d, Body: %s\n", rec.Code, rec.Body.String())

}
Output:
Status: 200, Body: Hello, charlie!

func (*Verifier) Sign ΒΆ

func (v *Verifier) Sign(dst []byte, payloadJSON []byte) []byte

Sign signs a raw JSON payload with the Verifier's configured algorithm (HS256, HS384, HS512, EdDSA, ES256) and appends the complete JWT token ("header.payload.signature") to dst with zero heap allocations (when dst has sufficient capacity).

Sign reports failure by returning nil: see TrySign for typed errors.

func (*Verifier) SignClaims ΒΆ

func (v *Verifier) SignClaims(dst []byte, sub, username []byte, iat, exp int64) []byte

SignClaims formats a standard JSON payload `{"sub":"...","username":"...","iat":...,"exp":...}` into Scratch and appends the signed JWT token to dst with zero heap allocations.

It returns nil on any failure condition (see TrySignClaims for typed errors).

func (*Verifier) SignHS256 ΒΆ

func (v *Verifier) SignHS256(dst []byte, payloadJSON []byte) []byte

SignHS256 signs a raw JSON payload and appends the complete JWT token ("header.payload.signature") to dst with zero heap allocations (when dst has sufficient capacity). It reports failure by returning nil: see TrySign for typed errors.

func (*Verifier) SignHS256WithScratch ΒΆ

func (v *Verifier) SignHS256WithScratch(dst []byte, payloadJSON []byte, st *Scratch) []byte

SignHS256WithScratch signs a raw JSON payload using a caller-supplied Scratch state. It reports failure by returning nil: see TrySignWithScratch for typed errors.

func (*Verifier) SignWithScratch ΒΆ

func (v *Verifier) SignWithScratch(dst []byte, payloadJSON []byte, st *Scratch) []byte

SignWithScratch signs a raw JSON payload using a caller-supplied Scratch state. It reports failure by returning nil: see TrySignWithScratch for typed errors.

func (*Verifier) TrySign ΒΆ added in v1.3.0

func (v *Verifier) TrySign(dst []byte, payloadJSON []byte) ([]byte, error)

TrySign signs a raw JSON payload with the Verifier's configured algorithm (HS256, HS384, HS512, EdDSA, ES256) and appends the complete JWT token ("header.payload.signature") to dst with zero heap allocations (when dst has sufficient capacity).

If signing fails, it returns nil and a typed error (ErrVerifierDestroyed, ErrNoSigningKey, ErrPayloadTooLarge, or ErrSigningFailed), leaving dst unmodified.

func (*Verifier) TrySignClaims ΒΆ added in v1.3.0

func (v *Verifier) TrySignClaims(dst []byte, sub, username []byte, iat, exp int64) ([]byte, error)

TrySignClaims formats a standard JSON payload `{"sub":"...","username":"...","iat":...,"exp":...}` into Scratch and appends the signed JWT token to dst with zero heap allocations.

It returns a typed error if the Verifier cannot sign, if RequireExp is set and exp <= 0, if sub is empty or exceeds 256 bytes (ClaimsLarge capacity limit), if username exceeds 256 bytes, or if the resulting payload exceeds MaxPayloadSize. (Note: standard Claims struct supports sub <= 64 bytes and username <= 128 bytes via Verify; callers verifying into standard Claims should remain within those bounds or use VerifyLarge).

func (*Verifier) TrySignClaimsWithScratch ΒΆ added in v1.3.0

func (v *Verifier) TrySignClaimsWithScratch(dst []byte, sub, username []byte, iat, exp int64, st *Scratch) ([]byte, error)

TrySignClaimsWithScratch is identical to TrySignClaims but uses caller-supplied Scratch state.

func (*Verifier) TrySignWithScratch ΒΆ added in v1.3.0

func (v *Verifier) TrySignWithScratch(dst []byte, payloadJSON []byte, st *Scratch) ([]byte, error)

TrySignWithScratch is identical to TrySign but uses a caller-supplied Scratch state.

func (*Verifier) Verify ΒΆ

func (v *Verifier) Verify(tok []byte, nowUnix int64, out *Claims) error

Verify validates the token signature, checks temporal ("exp", "nbf") and structural ("iss", "aud") claims against nowUnix, and populates out with the parsed claims. Safe for concurrent use across goroutines; performs 0 heap allocations (0 B/op, 0 allocs/op).

Example ΒΆ

ExampleVerifier_Verify demonstrates token verification with zero heap allocations, extracting standard claims, and checking sentinel errors using errors.Is.

package main

import (
	"errors"
	"fmt"

	"github.com/KaikeGM/zerojwt"
)

func main() {
	secret := []byte("super-secret-key-32-bytes-long!!")
	verifier := zerojwt.NewVerifier(secret)
	now := int64(1700000000)

	// Issue a valid token into a stack-allocated buffer (0 allocs/op)
	var tokBuf [512]byte
	token := verifier.SignClaims(tokBuf[:0], []byte("user_123"), []byte("alice"), now, now+3600)

	// Verify token into stack-allocated Claims
	var claims zerojwt.Claims
	if err := verifier.Verify(token, now, &claims); err != nil {
		switch {
		case errors.Is(err, zerojwt.ErrExpiredToken):
			fmt.Println("Token is expired")
		case errors.Is(err, zerojwt.ErrInvalidSignature):
			fmt.Println("Invalid signature")
		default:
			fmt.Printf("Validation error: %v\n", err)
		}
		return
	}

	fmt.Printf("Authenticated: %s (%s)\n", claims.Username(), claims.Subject())

	// Verifying an expired token triggers ErrExpiredToken
	err := verifier.Verify(token, now+7200, &claims)
	if errors.Is(err, zerojwt.ErrExpiredToken) {
		fmt.Println("Detected expired token successfully")
	}

}
Output:
Authenticated: alice (user_123)
Detected expired token successfully

func (*Verifier) VerifyBearer ΒΆ

func (v *Verifier) VerifyBearer(authHeader []byte, nowUnix int64, out *Claims) error

VerifyBearer strips the "Bearer " prefix from an HTTP Authorization header and verifies the token with zero heap allocations.

func (*Verifier) VerifyCustom ΒΆ

func (v *Verifier) VerifyCustom(tok []byte, nowUnix int64, onClaim func(key, val []byte, isStr bool) bool) error

VerifyCustom validates the token signature, checks "exp", "nbf", "iss", and "aud" against nowUnix, and invokes onClaim(key, val, isString) for every top-level JSON claim in the payload. Performs 0 heap allocations. Note: key and val slices are valid only during the onClaim callback.

Example ΒΆ

ExampleVerifier_VerifyCustom demonstrates inspecting arbitrary custom JSON attributes and claims directly from the verified payload buffer in 0 allocations.

package main

import (
	"fmt"

	"github.com/KaikeGM/zerojwt"
)

func main() {
	secret := []byte("super-secret-key-32-bytes-long!!")
	verifier := zerojwt.NewVerifier(secret)
	now := int64(1700000000)

	var tokBuf [512]byte
	b := verifier.NewSigner(tokBuf[:0])
	b.SetSubject([]byte("user_789")).
		SetClaimString("department", "engineering").
		SetClaimInt("clearance_level", 5).
		SetExpiresAt(now + 3600)

	token, _ := b.Sign()

	var dept string
	var clearance int64

	// Verify and extract arbitrary claims in a single pass with 0 heap allocations
	err := verifier.VerifyCustom(token, now, func(key, val []byte, isStr bool) bool {
		switch string(key) {
		case "department":
			dept = string(val)
		case "clearance_level":
			clearance, _ = zerojwt.ParsePositiveInt(val)
		}
		return true // continue scanning
	})

	if err != nil {
		fmt.Printf("VerifyCustom error: %v\n", err)
		return
	}

	fmt.Printf("Department: %s, Clearance Level: %d\n", dept, clearance)

}
Output:
Department: engineering, Clearance Level: 5

func (*Verifier) VerifyLarge ΒΆ

func (v *Verifier) VerifyLarge(tok []byte, nowUnix int64, out *ClaimsLarge) error

VerifyLarge validates the token and populates a ClaimsLarge struct (256B buffers per claim).

func (*Verifier) VerifyLargeWithScratch ΒΆ

func (v *Verifier) VerifyLargeWithScratch(tok []byte, nowUnix int64, out *ClaimsLarge, st *Scratch) error

VerifyLargeWithScratch validates the token into ClaimsLarge using a caller-supplied Scratch.

func (*Verifier) VerifyWithScratch ΒΆ

func (v *Verifier) VerifyWithScratch(tok []byte, nowUnix int64, out *Claims, st *Scratch) error

VerifyWithScratch performs the same verification as Verify using a caller-supplied Scratch, avoiding sync.Pool.Get/Put in single-threaded event loops (such as gnet with LockOSThread).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL