Skip to content

Basic Examples ​

HMAC Signing ​

The most common approach using symmetric key for signing and verification.

go
package main

import (
	"fmt"
	"time"

	"github.com/cybergodev/jwt"
)

func main() {
	cfg := jwt.DefaultConfig()
	cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
	cfg.AccessTokenTTL = 15 * time.Minute
	cfg.RefreshTokenTTL = 7 * 24 * time.Hour

	processor, err := jwt.New(cfg)
	if err != nil {
		panic(err)
	}
	defer processor.Close()

	// Issue
	claims := &jwt.Claims{
		UserID:      "user123",
		Username:    "alice",
		Role:        "admin",
		Permissions: []string{"read", "write", "delete"},
	}
	token, err := processor.Create(claims)
	if err != nil {
		panic(err)
	}
	fmt.Println("Access Token:", token)

	// Validate
	parsed, err := processor.Parse(token)
	if err != nil {
		panic(err)
	}
	fmt.Println("UserID:", parsed.UserID) // Output: user123
}

Access Token & Refresh Token ​

go
package main

import (
	"fmt"

	"github.com/cybergodev/jwt"
)

func main() {
	cfg := jwt.DefaultConfig()
	cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
	processor, err := jwt.New(cfg)
	if err != nil {
		panic(err)
	}
	defer processor.Close()

	claims := &jwt.Claims{
		UserID:   "user123",
		Username: "alice",
	}

	// Create access token (short-lived)
	accessToken, err := processor.Create(claims)
	if err != nil {
		panic(err)
	}

	// Create refresh token (long-lived)
	refreshToken, err := processor.CreateRefresh(claims)
	if err != nil {
		panic(err)
	}

	fmt.Println("Access Token:", accessToken)
	fmt.Println("Refresh Token:", refreshToken)

	// Use refresh token to get new access token
	newAccessToken, err := processor.Refresh(refreshToken)
	if err != nil {
		panic(err)
	}
	fmt.Println("New Access Token:", newAccessToken)
}

Token Revocation (Blacklist) ​

go
package main

import (
	"fmt"

	"github.com/cybergodev/jwt"
)

func main() {
	cfg := jwt.DefaultConfig()
	cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
	processor, err := jwt.New(cfg)
	if err != nil {
		panic(err)
	}
	defer processor.Close()

	claims := &jwt.Claims{UserID: "user123", Username: "alice"}
	token, err := processor.Create(claims)
	if err != nil {
		panic(err)
	}

	// Revoke token
	err = processor.Revoke(token)
	if err != nil {
		panic(err)
	}
	fmt.Println("Token revoked")

	// Validation will fail
	_, err = processor.Parse(token)
	fmt.Println("Parse error:", err) // token revoked

	// Check if revoked
	revoked, err := processor.IsRevoked(token)
	if err != nil {
		panic(err)
	}
	fmt.Println("Is revoked:", revoked) // Output: true
}

Rate Limiting ​

go
package main

import (
	"fmt"
	"time"

	"github.com/cybergodev/jwt"
)

func main() {
	cfg := jwt.DefaultConfig()
	cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
	cfg.EnableRateLimit = true
	cfg.RateLimitRate = 5 // Max 5 per minute
	cfg.RateLimitWindow = time.Minute

	processor, err := jwt.New(cfg)
	if err != nil {
		panic(err)
	}
	defer processor.Close()

	claims := &jwt.Claims{UserID: "user123", Username: "alice"}

	// Normal requests
	for i := 0; i < 5; i++ {
		_, err := processor.Create(claims)
		if err != nil {
			fmt.Printf("Request %d: %v\n", i+1, err)
		} else {
			fmt.Printf("Request %d: success\n", i+1)
		}
	}

	// 6th request is rate limited
	_, err = processor.Create(claims)
	fmt.Println("Request 6:", err) // rate limit exceeded
}

Audience Isolation ​

When ExpectedAudience is set, only tokens whose aud claim contains that value pass validation. This achieves inter-service token isolation in a microservice architecture — tokens issued by one service cannot be accepted by another.

go
package main

import (
	"errors"
	"fmt"

	"github.com/cybergodev/jwt"
)

func main() {
	cfg := jwt.DefaultConfig()
	cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
	cfg.ExpectedAudience = "billing-api" // Only accept tokens for billing-api

	processor, err := jwt.New(cfg)
	if err != nil {
		panic(err)
	}
	defer processor.Close()

	// Token with matching audience
	validClaims := &jwt.Claims{
		UserID: "user1",
		RegisteredClaims: jwt.RegisteredClaims{
			Audience: jwt.StringOrSlice{"billing-api"},
		},
	}
	validToken, err := processor.Create(validClaims)
	if err != nil {
		panic(err)
	}

	_, err = processor.Parse(validToken)
	if err != nil {
		panic(err)
	}
	fmt.Println("Matching audience valid:", err == nil) // Output: true

	// Token with wrong audience is rejected
	wrongClaims := &jwt.Claims{
		UserID: "user2",
		RegisteredClaims: jwt.RegisteredClaims{
			Audience: jwt.StringOrSlice{"admin-api"},
		},
	}
	wrongToken, err := processor.Create(wrongClaims)
	if err != nil {
		panic(err)
	}

	_, err = processor.Parse(wrongToken)
	fmt.Println("Wrong audience valid:", err == nil) // Output: false
	fmt.Println("Wrong audience error:", err)        // Output: token invalid audience
	fmt.Println("Is audience error:",
		errors.Is(err, jwt.ErrTokenInvalidAudience)) // Output: true
}

Microservice Scenarios

In a microservice architecture, set different ExpectedAudience values for different services (e.g. billing-api, user-api) so each service only accepts tokens intended for itself, preventing tokens from being abused across services.

Extra Extension Fields ​

The built-in Claims.Extra is a map[string]any for storing a small amount of optional additional information. The Processor performs deep validation on Extra during token creation (length, injection detection), so it is more convenient than custom Claims fields.

go
package main

import (
	"fmt"

	"github.com/cybergodev/jwt"
)

func main() {
	cfg := jwt.DefaultConfig()
	cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
	processor, err := jwt.New(cfg)
	if err != nil {
		panic(err)
	}
	defer processor.Close()

	// Store additional business fields in Extra (only string and []string values supported)
	claims := &jwt.Claims{
		UserID:   "user123",
		Username: "alice",
		Role:     "engineer",
		Extra: map[string]any{
			"team_id": "team-backend",
			"level":   "senior",
			"tags":    []string{"onboarding", "mentor"},
		},
	}
	token, err := processor.Create(claims)
	if err != nil {
		panic(err)
	}

	// Read Extra fields after validation
	parsed, err := processor.Parse(token)
	if err != nil {
		panic(err)
	}
	fmt.Println("UserID:", parsed.UserID) // Output: user123

	// Type-assert to read Extra values
	if teamID, ok := parsed.Extra["team_id"].(string); ok {
		fmt.Println("TeamID:", teamID) // Output: team-backend
	}
	if level, ok := parsed.Extra["level"].(string); ok {
		fmt.Println("Level:", level) // Output: senior
	}
	if tags, ok := parsed.Extra["tags"].([]string); ok {
		fmt.Println("Tags:", tags) // Output: [onboarding mentor]
	}
}

Extra Limitations

Extra allows at most 50 keys; values must be of type string or []string; nested maps are not supported. If you need more complex structures or custom validation, use a custom Claims type instead.

More Examples ​