Protect a Go net/http Service
The mcp-nethttp package provides Middleware — a standard func(http.Handler) http.Handler adapter. It extracts the bearer mandate from the Authorization header, verifies the ES256 signature against the zone JWKS, checks revocation, and enforces scope and agent requirements. Validated claims are stored in the request context and retrieved with ClaimsFromContext.
Prerequisites
Section titled “Prerequisites”- A Go HTTP service receiving requests from Caracal agents.
- The STS JWKS endpoint reachable from the Go process.
Install
Section titled “Install”go get github.com/garudex-labs/caracal/mcp-nethttpgo get github.com/garudex-labs/caracal/identityAdd the middleware
Section titled “Add the middleware”package main
import ( "net/http" "os"
mcpnethttp "github.com/garudex-labs/caracal/mcp-nethttp")
func main() { opts := mcpnethttp.Options{ Issuer: os.Getenv("CARACAL_STS_URL"), // e.g. http://sts:8080 Audience: "resource://inventory", ZoneID: os.Getenv("CARACAL_ZONE_ID"), RequiredScopes: []string{"inventory:read"}, RequireAgent: true, }
auth := mcpnethttp.Middleware(opts)
mux := http.NewServeMux() mux.Handle("/inventory", auth(http.HandlerFunc(listInventory))) mux.Handle("/inventory/", auth(http.HandlerFunc(getItem)))
http.ListenAndServe(":8080", mux)}Read claims from context
Section titled “Read claims from context”ClaimsFromContext retrieves the validated identity.Claims from the request context. It returns (claims, true) on success and (zero, false) when no claims are present (caller not behind the middleware):
import ( "net/http" mcpnethttp "github.com/garudex-labs/caracal/mcp-nethttp")
func listInventory(w http.ResponseWriter, r *http.Request) { claims, ok := mcpnethttp.ClaimsFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return }
// claims.Sub — principal subject // claims.ZoneID — zone the mandate was issued in // claims.Scope — space-separated scopes // claims.Sid — session ID (for revocation lookup) // claims.AgentSessionID — set when called from a spawned agent // claims.HopCount — delegation depth w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"principal":"` + claims.Sub + `"}`))}Middleware options
Section titled “Middleware options”| Field | Type | Required | Description |
|---|---|---|---|
Issuer | string | Yes | STS base URL; JWKS fetched from {Issuer}/.well-known/jwks.json |
Audience | string | Yes | Must match the aud claim in the mandate |
ZoneID | string | No | Rejects mandates from a different zone |
RequiredScopes | []string | No | All listed scopes must be in the mandate |
RequireAgent | bool | No | Rejects mandates without agent_session_id |
RequireDelegation | bool | No | Rejects mandates without delegation_edge_id |
RequireChainContains | []string | No | All listed application IDs must appear in delegation_chain |
MaxHopCount | int | No | Rejects mandates with hop_count above this value |
Revocations | revocation.Store | No | Checked against the mandate’s sid claim |
Per-route scope requirements
Section titled “Per-route scope requirements”Apply different scopes per route by creating separate middleware instances:
readAuth := mcpnethttp.Middleware(mcpnethttp.Options{ Issuer: stsURL, Audience: "resource://inventory", RequiredScopes: []string{"inventory:read"},})
writeAuth := mcpnethttp.Middleware(mcpnethttp.Options{ Issuer: stsURL, Audience: "resource://inventory", RequiredScopes: []string{"inventory:write"},})
mux := http.NewServeMux()mux.Handle("GET /inventory", readAuth(http.HandlerFunc(listInventory)))mux.Handle("POST /inventory", writeAuth(http.HandlerFunc(createItem)))Manual verification with the identity package
Section titled “Manual verification with the identity package”For services that do not use net/http (e.g., gRPC interceptors or custom servers), use identity.Verify directly:
import "github.com/garudex-labs/caracal/identity"
func verifyMandate(tokenStr string) (identity.Claims, error) { return identity.Verify(tokenStr, identity.Config{ Issuer: "http://sts:8080", Audience: "resource://inventory", ZoneID: "my-zone", RequiredScopes: []string{"inventory:read"}, RequireAgent: true, MaxHopCount: identity.DefaultMaxHopCount, })}Error types returned by identity.Verify:
| Error | Meaning |
|---|---|
identity.ErrTokenInvalid | JWT signature invalid, expired, or malformed |
identity.ErrZoneInvalid | Zone claim mismatch |
identity.ErrAgentIdentityRequired | RequireAgent is true but agent_session_id absent |
identity.ErrDelegationRequired | RequireDelegation is true but delegation_edge_id absent |
identity.ErrHopCountExceeded | hop_count exceeds MaxHopCount |
identity.ScopeMissingError{Scope: "…"} | Mandate is missing the named scope |
identity.ChainMismatchError{ApplicationID: "…"} | Required application not in delegation chain |
Complete example
Section titled “Complete example”package main
import ( "encoding/json" "log" "net/http" "os"
"github.com/garudex-labs/caracal/identity" mcpnethttp "github.com/garudex-labs/caracal/mcp-nethttp")
type Item struct { ID string `json:"id"` Name string `json:"name"`}
func listInventory(w http.ResponseWriter, r *http.Request) { claims, ok := mcpnethttp.ClaimsFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return }
if !identity.HasScope(claims.Scope, "inventory:read") { http.Error(w, "forbidden", http.StatusForbidden) return }
items := []Item{{ID: "1", Name: "Widget"}, {ID: "2", Name: "Gadget"}} w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(items)}
func main() { auth := mcpnethttp.Middleware(mcpnethttp.Options{ Issuer: os.Getenv("CARACAL_STS_URL"), Audience: "resource://inventory", ZoneID: os.Getenv("CARACAL_ZONE_ID"), RequiredScopes: []string{"inventory:read"}, RequireAgent: true, })
mux := http.NewServeMux() mux.Handle("GET /inventory", auth(http.HandlerFunc(listInventory)))
log.Println("listening on :8080") log.Fatal(http.ListenAndServe(":8080", mux))}What to read next
Section titled “What to read next”- Integrate the Go SDK — make outbound calls from protected handlers
- Author a Rego Policy — write the policy controlling which scopes are granted