---
title: Protect a Go net/http Service
description: Wrap Go HTTP handlers with the mcp-nethttp middleware to verify mandates and enforce scope requirements using Go context.Context.
pageType: workflow
---

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

- A Go HTTP service receiving requests from Caracal agents.
- The STS JWKS endpoint reachable from the Go process.

## Install

```bash
go get github.com/garudex-labs/caracal/packages/connectors/nethttp/go
go get github.com/garudex-labs/caracal/packages/identity/go
```

## Add the middleware

```go
package main

import (
    "net/http"
    "os"

    mcpnethttp "github.com/garudex-labs/caracal/packages/connectors/nethttp/go"
)

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

`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):

```go
import (
    "net/http"
    mcpnethttp "github.com/garudex-labs/caracal/packages/connectors/nethttp/go"
)

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

| 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

Apply different scopes per route by creating separate middleware instances:

```go
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

For services that do not use `net/http` (e.g., gRPC interceptors or custom servers), use `identity.Verify` directly:

```go
import "github.com/garudex-labs/caracal/packages/identity/go"

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

```go
package main

import (
    "encoding/json"
    "log"
    "net/http"
    "os"

    "github.com/garudex-labs/caracal/packages/identity/go"
    mcpnethttp "github.com/garudex-labs/caracal/packages/connectors/nethttp/go"
)

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

- [Integrate the Go SDK](./sdk-go/) — make outbound calls from protected handlers
- [Author a Rego Policy](./author-policy/) — write the policy controlling which scopes are granted
