Loading...
Loading...
Use when protecting Go HTTP API endpoints with JWT Bearer token validation or scope checks. Integrates go-jwt-middleware/v3 — use even if the user says "validate tokens in my Go API" or "secure my Go HTTP endpoints".
npx skill4agent add auth0/agent-skills go-jwt-middlewareAgent instruction: Before providing SDK setup instructions, fetch the latest release version by running:bashgh 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.
auth0-quickstartgo-auth0auth0-reactauth0-vueauth0-angularauth0-swiftauth0-androidauth0-react-nativeauth0-aspnetcore-apiexpress-jwtgo get github.com/auth0/go-jwt-middleware/v3
go get github.com/joho/godotenvAgent 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 thefile..envSTOP — 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?
- Automated — I'll use the Auth0 CLI to create the API resource and write the exact values to your .env file automatically.
- Manual — You create the API yourself in the Auth0 Dashboard (or via
) and provide me the Domain and Audience.auth0 apis createWhich do you prefer? (1 = Automated / 2 = Manual)"Do NOT proceed to any setup steps until the user has answered. Do NOT default to manual.
.envAgent instruction (Automated path checkpoints):When following the automated path, you MUST complete these checkpoints in order. Do NOT skip any:
- Check Auth0 CLI — verify
is installed.auth0- Check Auth0 login — run
to verify authentication.auth0 tenants list- 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<domain>in their terminal.auth0 tenants use <tenant>- Ask about API name and identifier — use
: "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.,AskUserQuestion).https://<module-name>- Ask about scopes — use
: "What scopes (permissions) does your API need? For example:AskUserQuestion,read:users,write:users. If you're not sure yet, I can start with common defaults and you can add more later." Wait for answer.read:products- Check for existing API — run
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.auth0 apis list- Create the API resource — using the name, identifier, and scopes from steps 4–5.
- Handle .env — if a
file already exists, ask before modifying it. Never read existing.envcontents (may contain secrets). If no.envexists, write one with.envandAUTH0_DOMAIN.AUTH0_AUDIENCE- Add
to.env— if not already present..gitignore- Proceed to code integration — skip Step 3 (already done) and go directly to Step 4 to write the middleware code.
# Using Auth0 CLI
auth0 apis create \
--name "My Go API" \
--identifier https://my-api.example.comAUTH0_DOMAIN=your-tenant.auth0.com
AUTH0_AUDIENCE=https://my-api.example.comhttps://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
with routes defined. In this case, do NOT replace their file with the template below. Instead:main.go
- Add the necessary imports (
,jwtmiddleware,jwks,validator,godotenv,net/url,os,context).strings- Add the
struct and methods.CustomClaims- Add the middleware setup code (issuer URL, JWKS provider, validator, middleware) near the top of
.main()- Ask which endpoints to protect (see below).
- 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.,for POST/DELETE), or is a valid JWT sufficient for all?"write:usersWait for the answer. If the user says "all" or "everything except health", protect all routes except(or whatever they specify as public). If they specify scope requirements per endpoint, implement per-route scope checks using/health.customClaims.HasScope()
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))
}middleware.CheckJWT()// 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)))Agent instruction: After writing the code, verify the build compiles:bashgo 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: "The build is still failing after several fix attempts. How would you like to proceed?"AskUserQuestion
- 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?
- Yes — I'll help you configure an M2M application to get test tokens.
- 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.
- Create new — I'll create a new M2M application and authorize it for this API.
- 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":bashauth0 apps create \ --name "<PROJECT_NAME> (Test App)" \ --type m2m \ --no-input --jsonParse the JSON to extract. Do NOT useclient_id— never expose client secrets in agent context. Then create a client grant:--reveal-secretsbashauth0 api post "client-grants" --data '{ "client_id": "<CLIENT_ID>", "audience": "<API_IDENTIFIER>", "scope": ["<SCOPES>"] }'If the user chose "Use existing": Ask for the Client ID. Then create a client grant to authorize it for this API:bashauth0 api post "client-grants" --data '{ "client_id": "<USER_PROVIDED_CLIENT_ID>", "audience": "<API_IDENTIFIER>", "scope": ["<SCOPES>"] }'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
on its own — it outputs the token to stdoutauth0 test token- Do NOT run
commands to thecurlendpoint on their own/oauth/token- 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:bashTEST_TOKEN=$(auth0 test token <CLIENT_ID> --audience <AUDIENCE> --scopes <SCOPE1,SCOPE2> 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/<ENDPOINT> -H "Authorization: Bearer $TEST_TOKEN"Security guarantees of this approach:
captures stdout — the token is consumed into the variable, not displayed$(...) extracts only the JWT pattern (ey...) — no surrounding output leaksgrep -o confirms success by printing LENGTH only, never the valueecho "Token acquired (${#TEST_TOKEN} chars)"- The shell variable
exists only for the duration of that single command chain — it dies immediately after$TEST_TOKEN- Agent sees only:
+ the API response body (JSON)"Token acquired (834 chars)"- No file is written, no env is exported, nothing persists
Rules for using this pattern:
- ONLY use when the user explicitly asks to test (e.g., "test it", "run the tests", "verify endpoints work")
- Always chain token acquisition + curl in a SINGLE
command — never separate them into two Bash calls&&- To test multiple endpoints, chain multiple curls in the same command:
bashTEST_TOKEN=$(auth0 test token <CLIENT_ID> --audience <AUDIENCE> --scopes <SCOPE1,SCOPE2> 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"- NEVER add
,echo $TEST_TOKEN, or any command that would print the raw token valueprintf $TEST_TOKEN- If the token acquisition fails (empty variable), the
check will halt the chain — report to the user that the M2M app may not be authorized[ -n "$TEST_TOKEN" ]- Client ID is REQUIRED — the
command requires a Client ID to be passed as the first argument. This MUST be theauth0 test tokenobtained from the M2M app setup step (create new or use existing). If the M2M step has not been completed yet (no Client ID available), do NOT attempt to run the test token command. Instead, ask the user: "I need an M2M application Client ID to get a test token. Would you like me to create one or do you have an existing one?" — then complete the M2M setup first.client_idIf the user does NOT ask to test, just provide the commands for them to run manually:auth0 test token <CLIENT_ID> --audience <AUDIENCE> --scopes <SCOPE1,SCOPE2> curl http://localhost:8080/<endpoint> -H "Authorization: Bearer <PASTE_TOKEN_HERE>"
go run .curl http://localhost:8080/api/publiccurl http://localhost:8080/api/privateTEST_TOKEN=$(auth0 test token <M2M_CLIENT_ID> --audience https://my-api.example.com --scopes <SCOPE1,SCOPE2> 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/api/private -H "Authorization: Bearer $TEST_TOKEN"| Mistake | Fix |
|---|---|
| Created Application instead of API in Auth0 | Must create API resource in Auth0 Dashboard → Applications → APIs |
| Audience doesn't match API Identifier | Must exactly match the API Identifier set in Auth0 Dashboard |
Domain includes | Use |
| Using v2 positional parameters instead of v3 options | v3 uses |
| Missing trailing slash on issuer URL | Issuer must be |
Checking | Use custom claims struct with |
Missing | Add |
Using | Use |
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}handler := corsMiddleware(mux)
log.Fatal(http.ListenAndServe(":8080", handler))auth0-quickstartauth0-mfavalidator.WithKeyFunc(provider.KeyFunc)validator.WithAlgorithm(validator.RS256)validator.WithIssuer(url)validator.WithAudience(aud)validator.WithCustomClaims(fn)validator.WithAllowedClockSkew(d)jwtmiddleware.GetClaims[*validator.ValidatedClaims](r.Context())claims.RegisteredClaims.Subjectclaims.CustomClaims.(*CustomClaims).Scopeclaims.CustomClaims.(*CustomClaims).Permissionsmiddleware.CheckJWT(handler)