# Go Jwt Middleware

> >

- **Type:** Skill
- **Install:** `agentstack add skill-auth0-agent-skills-go-jwt-middleware`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [auth0](https://agentstack.voostack.com/s/auth0)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** Apache-2.0
- **Upstream author:** [auth0](https://github.com/auth0)
- **Source:** https://github.com/auth0/agent-skills/tree/main/plugins/auth0/skills/go-jwt-middleware

## Install

```sh
agentstack add skill-auth0-agent-skills-go-jwt-middleware
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Go JWT Middleware Integration

Protect Go HTTP API endpoints with JWT access token validation using github.com/auth0/go-jwt-middleware/v3.

> **Agent instruction:** Before providing SDK setup instructions, fetch the latest release version by running:
> ```bash
> gh api repos/auth0/go-jwt-middleware/releases/latest --jq '.tag_name'
> ```
> Use the returned version in all dependency lines instead of any hardcoded version below.

---

## Prerequisites

- Go 1.21 or higher
- Auth0 API configured (not Application - must be API resource)
- If you don't have Auth0 set up yet, use the `auth0-quickstart` skill first

## When NOT to Use

- **Go server-rendered web applications** - Use `go-auth0` for session-based web apps
- **Single Page Applications** - Use `auth0-react`, `auth0-vue`, or `auth0-angular` for client-side auth
- **Mobile applications** - Use `auth0-swift`, `auth0-android`, or `auth0-react-native`
- **Non-Go backends** - Use `auth0-aspnetcore-api` for .NET, `express-jwt` for Node.js

---

## Quick Start Workflow

### 1. Install SDK

```bash
go get github.com/auth0/go-jwt-middleware/v3
go get github.com/joho/godotenv
```

### 2. Create Auth0 API

You need an **API** (not Application) in Auth0.

> **Agent instruction:** If the user's prompt already provides Auth0 credentials (domain and audience), use them directly — skip the setup choice question below and proceed to Step 3 to write the `.env` file.
>
> **STOP — ask the user before proceeding.**
>
> Ask exactly this question and wait for their answer before doing anything else:
>
> > "How would you like to create the Auth0 API resource?
> > 1. **Automated** — I'll use the Auth0 CLI to create the API resource and write the exact values to your .env file automatically.
> > 2. **Manual** — You create the API yourself in the Auth0 Dashboard (or via `auth0 apis create`) and provide me the Domain and Audience.
> >
> > Which do you prefer? (1 = Automated / 2 = Manual)"
>
> Do NOT proceed to any setup steps until the user has answered. Do NOT default to manual.

**If the user chose Automated**, follow the [Setup Guide](references/setup.md) for the "Initial Setup" section (steps 1–6). The automated path writes `.env` for you — skip Step 3 below and proceed directly to Step 4.

> **Agent instruction (Automated path checkpoints):**
>
> When following the automated path, you MUST complete these checkpoints in order. Do NOT skip any:
>
> 1. **Check Auth0 CLI** — verify `auth0` is installed.
> 2. **Check Auth0 login** — run `auth0 tenants list` to verify authentication.
> 3. **Confirm active tenant** — show the user which tenant is active and ask: _"Your active Auth0 tenant is ``. Is this the correct tenant?"_ Wait for confirmation. If they say no, ask them to run `auth0 tenants use ` in their terminal.
> 4. **Ask about API name and identifier** — use `AskUserQuestion`: _"What would you like to name your Auth0 API, and what identifier (audience) should it use? For example: Name: 'My Go API', Identifier: 'https://my-api.example.com'. The identifier is a logical URI that doesn't need to resolve — it just uniquely identifies your API."_ Wait for answer. If the user is unsure, suggest deriving the identifier from the project's module name in go.mod (e.g., `https://`).
> 5. **Ask about scopes** — use `AskUserQuestion`: _"What scopes (permissions) does your API need? For example: `read:users`, `write:users`, `read:products`. If you're not sure yet, I can start with common defaults and you can add more later."_ Wait for answer.
> 6. **Check for existing API** — run `auth0 apis list` and check if an API with the intended identifier already exists. If it does, ask the user whether to reuse it or create a new one with a different identifier.
> 7. **Create the API resource** — using the name, identifier, and scopes from steps 4–5.
> 8. **Handle .env** — if a `.env` file already exists, ask before modifying it. Never read existing `.env` contents (may contain secrets). If no `.env` exists, write one with `AUTH0_DOMAIN` and `AUTH0_AUDIENCE`.
> 9. **Add `.env` to `.gitignore`** — if not already present.
> 10. **Proceed to code integration** — skip Step 3 (already done) and go directly to Step 4 to write the middleware code.

**If the user chose Manual**, follow the [Setup Guide](references/setup.md) (Manual Setup section) for full instructions. Then continue with Step 3 below.

Quick reference for manual API creation:

```bash
# Using Auth0 CLI
auth0 apis create \
  --name "My Go API" \
  --identifier https://my-api.example.com
```

Or create manually in Auth0 Dashboard → Applications → APIs

### 3. Configure .env

```env
AUTH0_DOMAIN=your-tenant.auth0.com
AUTH0_AUDIENCE=https://my-api.example.com
```

**Important:** Domain must NOT include `https://`. The middleware constructs the issuer URL automatically.

### 4. Configure main.go

> **Agent instruction (integrating with existing code):**
>
> Before writing code, determine whether you are:
> - **A) Adding auth to an existing project** — the user already has a `main.go` with routes defined. In this case, do NOT replace their file with the template below. Instead:
>   1. Add the necessary imports (`jwtmiddleware`, `jwks`, `validator`, `godotenv`, `net/url`, `os`, `context`, `strings`).
>   2. Add the `CustomClaims` struct and methods.
>   3. Add the middleware setup code (issuer URL, JWKS provider, validator, middleware) near the top of `main()`.
>   4. Ask which endpoints to protect (see below).
>   5. Wrap the specified handlers with `middleware.CheckJWT()`.
>
> - **B) Creating a new project from scratch** — use the full template below as a starting point.
>
> **STOP — ask which endpoints to protect:**
>
> If the user's request does NOT explicitly specify which endpoints to protect, ask:
>
> > "Which endpoints should require authentication? For example:
> > - **All except health/public** — protect everything, leave only specific public routes open
> > - **Specific routes** — tell me which routes need auth
> >
> > Also, do any endpoints need specific scope/permission checks (e.g., `write:users` for POST/DELETE), or is a valid JWT sufficient for all?"
>
> Wait for the answer. If the user says "all" or "everything except health", protect all routes except `/health` (or whatever they specify as public). If they specify scope requirements per endpoint, implement per-route scope checks using `customClaims.HasScope()`.

```go
package main

import (
	"context"
	"encoding/json"
	"log"
	"net/http"
	"net/url"
	"os"
	"strings"

	jwtmiddleware "github.com/auth0/go-jwt-middleware/v3"
	"github.com/auth0/go-jwt-middleware/v3/jwks"
	"github.com/auth0/go-jwt-middleware/v3/validator"
	"github.com/joho/godotenv"
)

// CustomClaims contains custom data we want from the token.
type CustomClaims struct {
	Scope       string   `json:"scope"`
	Permissions []string `json:"permissions"`
}

func (c CustomClaims) Validate(ctx context.Context) error {
	return nil
}

func (c CustomClaims) HasScope(expectedScope string) bool {
	for _, scope := range strings.Split(c.Scope, " ") {
		if scope == expectedScope {
			return true
		}
	}
	return false
}

func main() {
	if err := godotenv.Load(); err != nil {
		log.Fatalf("Error loading .env file: %v", err)
	}

	issuerURL, err := url.Parse("https://" + os.Getenv("AUTH0_DOMAIN") + "/")
	if err != nil {
		log.Fatalf("Failed to parse issuer URL: %v", err)
	}

	provider, err := jwks.NewCachingProvider(
		jwks.WithIssuerURL(issuerURL),
	)
	if err != nil {
		log.Fatalf("Failed to set up JWKS provider: %v", err)
	}

	jwtValidator, err := validator.New(
		validator.WithKeyFunc(provider.KeyFunc),
		validator.WithAlgorithm(validator.RS256),
		validator.WithIssuer(issuerURL.String()),
		validator.WithAudience(os.Getenv("AUTH0_AUDIENCE")),
		validator.WithCustomClaims(func() validator.CustomClaims {
			return &CustomClaims{}
		}),
	)
	if err != nil {
		log.Fatalf("Failed to set up JWT validator: %v", err)
	}

	middleware, err := jwtmiddleware.New(
		jwtmiddleware.WithValidator(jwtValidator),
	)
	if err != nil {
		log.Fatalf("Failed to set up JWT middleware: %v", err)
	}

	mux := http.NewServeMux()

	// Public endpoint - no authentication
	mux.HandleFunc("/api/public", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{"message": "Hello from a public endpoint!"})
	})

	// Protected endpoint - requires valid JWT
	mux.Handle("/api/private", middleware.CheckJWT(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		claims, err := jwtmiddleware.GetClaims[*validator.ValidatedClaims](r.Context())
		if err != nil {
			http.Error(w, `{"message":"Failed to get token claims."}`, http.StatusInternalServerError)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{
			"message": "Hello from a private endpoint!",
			"userId":  claims.RegisteredClaims.Subject,
		})
	})))

	// Protected + scoped endpoint - requires JWT with specific scope
	mux.Handle("/api/private-scoped", middleware.CheckJWT(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		claims, err := jwtmiddleware.GetClaims[*validator.ValidatedClaims](r.Context())
		if err != nil {
			http.Error(w, `{"message":"Failed to get token claims."}`, http.StatusInternalServerError)
			return
		}
		customClaims := claims.CustomClaims.(*CustomClaims)
		if !customClaims.HasScope("read:messages") {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusForbidden)
			json.NewEncoder(w).Encode(map[string]string{"message": "Insufficient scope."})
			return
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{"message": "Hello from a scoped endpoint!"})
	})))

	log.Println("Server listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", mux))
}
```

### 5. Protect Endpoints

Use `middleware.CheckJWT()` to wrap handlers that require authentication:

```go
// Public endpoint - no authentication
mux.HandleFunc("/api/public", publicHandler)

// Protected endpoint - requires valid JWT
mux.Handle("/api/private", middleware.CheckJWT(http.HandlerFunc(privateHandler)))

// Protected + scoped - requires JWT with specific permission
mux.Handle("/api/private-scoped", middleware.CheckJWT(http.HandlerFunc(privateScopedHandler)))
```

### 6. Test API

> **Agent instruction:** After writing the code, verify the build compiles:
> ```bash
> go build ./...
> ```
> If compilation fails, diagnose the error and fix it. Repeat up to 5-6 times.
>
> **Failcheck:** If the build still fails after 5-6 fix attempts, stop and ask the user using `AskUserQuestion`:
> _"The build is still failing after several fix attempts. How would you like to proceed?"_
> - **Let me continue fixing iteratively**
> - **Fix it manually** — I'll show the remaining errors
> - **Skip build verification** — proceed without a successful build
>
> Repeat this check after every 5-6 iterations if errors persist.

> **Agent instruction: STOP — ask after build succeeds.**
>
> Once the build compiles successfully, ask the user:
>
> > "Your API is set up and compiles successfully. Would you like me to help you set up testing?
> > 1. **Yes** — I'll help you configure an M2M application to get test tokens.
> > 2. **No** — I'm done for now.
> >
> > Which do you prefer?"
>
> Do NOT proceed with testing setup unless the user says yes. If they say no, summarize what was done and stop.

> **Agent instruction (M2M app setup — only if user wants testing):**
>
> If the user chose to set up testing, ask:
>
> > "To test your protected endpoints, you need a Machine-to-Machine (M2M) application authorized to request tokens for this API.
> > 1. **Create new** — I'll create a new M2M application and authorize it for this API.
> > 2. **Use existing** — You already have an M2M application. Provide the Client ID and I'll authorize it for this API.
> >
> > Which do you prefer? (1 = Create new / 2 = Use existing)"
>
> Do NOT proceed until the user answers. Do NOT silently pick an existing application from the tenant.
>
> **If the user chose "Create new":**
> ```bash
> auth0 apps create \
>   --name " (Test App)" \
>   --type m2m \
>   --no-input --json
> ```
> Parse the JSON to extract `client_id`. Do NOT use `--reveal-secrets` — never expose client secrets in agent context.
> Then create a client grant:
> ```bash
> auth0 api post "client-grants" --data '{
>   "client_id": "",
>   "audience": "",
>   "scope": [""]
> }'
> ```
>
> **If the user chose "Use existing":**
> Ask for the Client ID. Then create a client grant to authorize it for this API:
> ```bash
> auth0 api post "client-grants" --data '{
>   "client_id": "",
>   "audience": "",
>   "scope": [""]
> }'
> ```
> If the grant already exists (409 conflict), that's fine — the app is already authorized.

> **Agent instruction (TOKEN ISOLATION — CRITICAL):**
>
> The agent MUST NEVER directly see or display access token values. Token security rules:
> - Do NOT run `auth0 test token` on its own — it outputs the token to stdout
> - Do NOT run `curl` commands to the `/oauth/token` endpoint on their own
> - Do NOT ask the user to paste their token into the conversation
> - Do NOT echo, print, or log the token value
> - Do NOT store the token in a file
>
> **Secure testing approach (single-command chain):**
>
> If the user explicitly asks to test the protected endpoints, the agent MAY use a single-command chain that captures the token into a shell variable and immediately uses it — the token value is never printed or visible to the agent:
>
> ```bash
> TEST_TOKEN=$(auth0 test token  --audience  --scopes  2>/dev/null | grep -o 'ey[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*') && \
> [ -n "$TEST_TOKEN" ] && echo "Token acquired (${#TEST_TOKEN} chars)" && \
> curl -s http://localhost:8080/ -H "Authorization: Bearer $TEST_TOKEN"
> ```
>
> **Security guarantees of this approach:**
> - `$(...)` captures stdout — the token is consumed into the variable, not displayed
> - `grep -o` extracts only the JWT pattern (ey...) — no surrounding output leaks
> - `echo "Token acquired (${#TEST_TOKEN} chars)"` confirms success by printing LENGTH only, never the value
> - The shell variable `$TEST_TOKEN` exists only for the duration of that single command chain — it dies immediately after
> - Agent sees only: `"Token acquired (834 chars)"` + the API response body (JSON)
> - No file is written, no env is exported, nothing persists
>
> **Rules for using this pattern:**
> 1. ONLY use when the user explicitly asks to test (e.g., "test it", "run the tests", "verify endpoints work")
> 2. Always chain token acquisition + curl in a SINGLE `&&` command — never separate them into two Bash calls
> 3. To test multiple endpoints, chain multiple curls in the same command:
>    ```bash
>    TEST_TOKEN=$(auth0 test token  --audience  --scopes  2>/dev/null | grep -o 'ey[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*') && \
>    [ -n "$TEST_TOKEN" ] && echo "Token acquired (${#TEST_TOKEN} chars)" && \
>    echo "=== GET /users ===" && \
>    curl -s http://localhost:8080/users -H "Authorization: Bearer $TEST_TOKEN" && \
>    echo "" && echo "=== POST /users ===" && \
>    curl -s -X POST http://localhost:8080/users -H "Authorization: Bearer $TEST_TOKEN" -d '{"id":"99","name":"Test","email":"test@example.com"}' && \
>    echo "" && echo "=== GET /products ===" && \
>    curl -s http://localhost:8080/products -H "Authorization: Bearer $TEST_TOKEN"
>    ```
> 4. NEVER add `echo $TEST_TOKEN`, `printf $TEST_TOKEN`, or any command that would print the raw token value
> 5. If the token acquisition fails (empty variable), the `[ -n "$TEST_TOKEN" ]` check will halt the chain — report to the user that the M2M app may not be authorized
> 6. **Client ID is REQUIRED** — the `auth0 test token` command requires a Client ID to be passed as the first argument. This MUST be the `client_id` obtained from the M2M app setup

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [auth0](https://github.com/auth0)
- **Source:** [auth0/agent-skills](https://github.com/auth0/agent-skills)
- **License:** Apache-2.0

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-auth0-agent-skills-go-jwt-middleware
- Seller: https://agentstack.voostack.com/s/auth0
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
