Skip to content

Latest commit

 

History

History
393 lines (308 loc) · 10.7 KB

File metadata and controls

393 lines (308 loc) · 10.7 KB

Go SDK Middleware Guide

This guide covers integrating PEAC receipt verification with popular Go HTTP frameworks.

Version: 0.9.26+

Overview

The PEAC Go SDK provides middleware for verifying receipts on incoming HTTP requests. The middleware:

  • Extracts the PEAC-Receipt header from requests
  • Verifies the JWS signature using Ed25519
  • Validates claims (issuer, audience, timestamps)
  • Injects verified claims into request context

Installation

go get github.com/peacprotocol/peac-go

net/http (Standard Library)

The 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 Router

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))
}

Route Groups with Different Configs

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 Framework

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")
}

gin Route Groups

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")
}

Configuration Reference

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

Error Handling

Default Error Behavior

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"
}

Custom Error Handler

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",
        })
    },
}

Error Codes

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

Best Practices

1. Use Appropriate MaxAge

// High-frequency APIs: shorter validity
cfg := middleware.Config{
    MaxAge: 15 * time.Minute,
}

// Batch processing: longer validity
cfg := middleware.Config{
    MaxAge: 24 * time.Hour,
}

2. Configure Clock Skew for Distributed Systems

cfg := middleware.Config{
    ClockSkew: time.Minute, // More tolerance for distributed systems
}

3. Cache JWKS Responses

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,
})

4. Log Verification Failures

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)
    },
}

See Also