This guide covers integrating PEAC receipt verification with popular Go HTTP frameworks.
Version: 0.9.26+
The PEAC Go SDK provides middleware for verifying receipts on incoming HTTP requests. The middleware:
- Extracts the
PEAC-Receiptheader from requests - Verifies the JWS signature using Ed25519
- Validates claims (issuer, audience, timestamps)
- Injects verified claims into request context
go get github.com/peacprotocol/peac-goThe middleware works directly with the standard http.Handler interface:
package main
import (
"net/http"
"time"
peac "github.com/peacprotocol/peac-go"
"github.com/peacprotocol/peac-go/middleware"
)
func main() {
cfg := middleware.Config{
Issuer: "https://publisher.example",
Audience: "https://agent.example",
MaxAge: time.Hour,
JWKSURL: "https://publisher.example/.well-known/jwks.json",
}
mux := http.NewServeMux()
mux.HandleFunc("/api/content", contentHandler)
// Wrap with PEAC middleware
handler := middleware.Middleware(cfg)(mux)
http.ListenAndServe(":8080", handler)
}
func contentHandler(w http.ResponseWriter, r *http.Request) {
// Access verified claims from context
claims := middleware.GetClaims(r)
if claims == nil {
http.Error(w, "No receipt", http.StatusUnauthorized)
return
}
// Use claims for authorization
w.Write([]byte("Access granted for receipt: " + claims.ReceiptID))
}chi is fully compatible with http.Handler, so the middleware works directly:
package main
import (
"net/http"
"time"
"github.com/go-chi/chi/v5"
chiMiddleware "github.com/go-chi/chi/v5/middleware"
peac "github.com/peacprotocol/peac-go"
"github.com/peacprotocol/peac-go/middleware"
)
func main() {
r := chi.NewRouter()
// Standard chi middleware
r.Use(chiMiddleware.Logger)
r.Use(chiMiddleware.Recoverer)
// PEAC receipt verification
r.Use(middleware.Middleware(middleware.Config{
Issuer: "https://publisher.example",
Audience: "https://agent.example",
MaxAge: time.Hour,
JWKSURL: "https://publisher.example/.well-known/jwks.json",
}))
// Routes
r.Get("/api/content", contentHandler)
r.Get("/api/search", searchHandler)
http.ListenAndServe(":8080", r)
}
func contentHandler(w http.ResponseWriter, r *http.Request) {
claims := middleware.GetClaims(r)
if claims == nil {
http.Error(w, "Receipt required", http.StatusUnauthorized)
return
}
// Check purpose (v0.9.24+)
if claims.PurposeEnforced != "" {
w.Header().Set("X-Purpose-Enforced", claims.PurposeEnforced)
}
w.Write([]byte("Content accessed with receipt: " + claims.ReceiptID))
}
func searchHandler(w http.ResponseWriter, r *http.Request) {
claims := middleware.GetClaims(r)
if claims == nil {
http.Error(w, "Receipt required", http.StatusUnauthorized)
return
}
w.Write([]byte("Search executed with receipt: " + claims.ReceiptID))
}Use chi's route groups for different verification policies:
func main() {
r := chi.NewRouter()
// Public routes (no receipt required)
r.Group(func(r chi.Router) {
r.Get("/health", healthHandler)
r.Get("/", homeHandler)
})
// Protected routes (receipt required)
r.Group(func(r chi.Router) {
r.Use(middleware.Middleware(middleware.Config{
Issuer: "https://publisher.example",
Audience: "https://agent.example",
MaxAge: time.Hour,
}))
r.Get("/api/content", contentHandler)
r.Get("/api/search", searchHandler)
})
// Premium routes (stricter validation)
r.Group(func(r chi.Router) {
r.Use(middleware.Middleware(middleware.Config{
Issuer: "https://publisher.example",
Audience: "https://premium.agent.example",
MaxAge: 15 * time.Minute, // Shorter validity
ClockSkew: 10 * time.Second,
}))
r.Get("/api/premium", premiumHandler)
})
http.ListenAndServe(":8080", r)
}gin uses its own handler type, so we need an adapter:
package main
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
peac "github.com/peacprotocol/peac-go"
"github.com/peacprotocol/peac-go/middleware"
)
// GinMiddleware adapts PEAC middleware for gin
func GinMiddleware(cfg middleware.Config) gin.HandlerFunc {
peacMiddleware := middleware.Middleware(cfg)
return func(c *gin.Context) {
// Create a wrapped handler that calls gin's Next()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Copy claims to gin context
if claims := middleware.GetClaims(r); claims != nil {
c.Set("peac_claims", claims)
}
if result := middleware.GetResult(r); result != nil {
c.Set("peac_result", result)
}
c.Next()
})
// Apply PEAC middleware
peacMiddleware(handler).ServeHTTP(c.Writer, c.Request)
}
}
// GetPEACClaims retrieves verified claims from gin context
func GetPEACClaims(c *gin.Context) *peac.PEACReceiptClaims {
if claims, exists := c.Get("peac_claims"); exists {
return claims.(*peac.PEACReceiptClaims)
}
return nil
}
func main() {
r := gin.Default()
// Apply PEAC middleware
r.Use(GinMiddleware(middleware.Config{
Issuer: "https://publisher.example",
Audience: "https://agent.example",
MaxAge: time.Hour,
JWKSURL: "https://publisher.example/.well-known/jwks.json",
}))
r.GET("/api/content", func(c *gin.Context) {
claims := GetPEACClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Receipt required"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Access granted",
"receipt_id": claims.ReceiptID,
"purpose": claims.PurposeEnforced,
})
})
r.Run(":8080")
}func main() {
r := gin.Default()
// Public routes
public := r.Group("/")
{
public.GET("/health", healthHandler)
}
// Protected routes
api := r.Group("/api")
api.Use(GinMiddleware(middleware.Config{
Issuer: "https://publisher.example",
Audience: "https://agent.example",
MaxAge: time.Hour,
}))
{
api.GET("/content", contentHandler)
api.GET("/search", searchHandler)
}
r.Run(":8080")
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
Issuer |
string |
Yes | - | Expected issuer (iss claim) |
Audience |
string |
Yes | - | Expected audience (aud claim) |
MaxAge |
time.Duration |
No | 1 hour | Maximum receipt age |
JWKSURL |
string |
No | - | JWKS endpoint for key discovery |
ClockSkew |
time.Duration |
No | 30s | Tolerance for clock differences |
ClaimsContextKey |
string |
No | peac_claims |
Context key for claims |
ResultContextKey |
string |
No | peac_result |
Context key for full result |
ErrorHandler |
func(...) |
No | - | Custom error handler |
By default, the middleware returns RFC 9457 Problem Details responses:
{
"type": "https://www.peacprotocol.org/errors/receipt_expired",
"title": "Receipt Expired",
"status": 401,
"detail": "Receipt issued at 2026-01-01T00:00:00Z has exceeded max age"
}cfg := middleware.Config{
Issuer: "https://publisher.example",
Audience: "https://agent.example",
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
// Log the error
log.Printf("PEAC verification failed: %v", err)
// Custom response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{
"error": "Authentication failed",
"code": "RECEIPT_INVALID",
})
},
}| Code | HTTP Status | Description |
|---|---|---|
receipt_missing |
401 | No PEAC-Receipt header |
receipt_malformed |
400 | Invalid JWS format |
receipt_expired |
401 | Receipt past max age |
receipt_not_yet_valid |
401 | issued_at in future |
signature_invalid |
401 | Ed25519 signature failed |
issuer_mismatch |
403 | Wrong issuer |
audience_mismatch |
403 | Wrong audience |
jwks_fetch_failed |
503 | Cannot fetch keys |
// High-frequency APIs: shorter validity
cfg := middleware.Config{
MaxAge: 15 * time.Minute,
}
// Batch processing: longer validity
cfg := middleware.Config{
MaxAge: 24 * time.Hour,
}cfg := middleware.Config{
ClockSkew: time.Minute, // More tolerance for distributed systems
}The middleware automatically caches JWKS responses. Configure the cache TTL:
import "github.com/peacprotocol/peac-go/jwks"
// Custom cache configuration
cache := jwks.NewCache(jwks.CacheConfig{
TTL: 5 * time.Minute,
MaxEntries: 100,
})cfg := middleware.Config{
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("[PEAC] Verification failed for %s %s: %v",
r.Method, r.URL.Path, err)
// Default error response
middleware.DefaultErrorHandler(w, r, err)
},
}- Go SDK Reference - Core verification API
- PEAC-Receipt Header - Wire protocol details
- Purpose Headers - Purpose enforcement (v0.9.24+)