mcpField
G
go-jwt-middleware

go-jwt-middleware

github.com/auth0安全55336 次安装1 天前更新
代码仓库

关于 go-jwt-middleware

用于为 Go HTTP API 添加 Auth0 JWT 验证的编码技能,指导 Agent 配置 API、集成中间件、保护端点并验证构建。

主要功能

  1. 01配置 Auth0 JWT 中间件
  2. 02设置 API 资源
  3. 03集成受保护路由
  4. 04按作用域检查权限
  5. 05验证 Go 构建

适用场景

  1. 01为 Go API 路由添加 JWT 身份验证
  2. 02按权限作用域保护端点
  3. 03配置 Auth0 凭据和 API
  4. 04验证中间件集成可编译

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/auth0/agent-skills --skill go-jwt-middleware

安装后在 Claude Code 中输入/go-jwt-middleware确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
api.md
integration.md
setup.md

详细介绍

Go JWT 中间件集成

使用 github.com/auth0/go-jwt-middleware/v3 验证 JWT 访问令牌,保护 Go HTTP API 端点。

Agent 指令: 提供 SDK 设置说明前,请运行以下命令获取最新发布版本:

gh api repos/auth0/go-jwt-middleware/releases/latest --jq '.tag_name'

在所有依赖项行中使用返回的版本,不要使用下方任何硬编码的版本。


前置条件

  • Go 1.21 或更高版本
  • 已配置 Auth0 API(不能是 Application,必须是 API 资源)
  • 如果尚未设置 Auth0,请先使用 auth0-quickstart 技能

不适用场景

  • Go 服务端渲染的 Web 应用——基于会话的 Web 应用请使用 go-auth0
  • 单页应用——客户端身份验证请使用 auth0-react、auth0-vue 或 auth0-angular
  • 移动应用——请使用 auth0-swift、auth0-android 或 auth0-react-native
  • 非 Go 后端——.NET 请使用 auth0-aspnetcore-api,Node.js 请使用 express-jwt

快速入门工作流

1. 安装 SDK
go get github.com/auth0/go-jwt-middleware/v3
go get github.com/joho/godotenv
2. 创建 Auth0 API

你需要在 Auth0 中创建一个 API(而不是 Application)。

Agent 指令: 如果用户的提示词已提供 Auth0 凭据(domain 和 audience),请直接使用这些凭据——跳过下面的设置选项问题,并继续执行步骤 3,编写 .env 文件。

停止——继续操作前先询问用户。

请原样询问以下问题,并等待用户回答后再进行其他操作:

"你希望如何创建 Auth0 API 资源?

  1. 自动 — 我将使用 Auth0 CLI 创建 API 资源,并自动将确切的值写入你的 .env 文件。
  2. 手动 — 你在 Auth0 Dashboard 中自行创建 API(或通过 auth0 apis create 创建),然后向我提供 Domain 和 Audience。

你更倾向哪种方式?(1 = 自动 / 2 = 手动)"

在用户回答之前,不要继续任何设置步骤。不要默认选择手动方式。

如果用户选择自动方式,请按照设置指南中“初始设置”部分的步骤 1–6 操作。自动方式会为你写入 .env——跳过下面的步骤 3,直接继续步骤 4。

Agent 指令(自动方式检查点):

使用自动方式时,必须按顺序完成以下检查点。不要跳过任何一项:

  1. 检查 Auth0 CLI — 确认已安装 auth0。
  2. 检查 Auth0 登录状态 — 运行 auth0 tenants list 验证身份验证。
  3. 确认当前租户 — 告知用户当前启用的租户,并询问:“当前启用的 Auth0 租户是 <domain>。这是正确的租户吗?” 等待确认。如果用户回答不是,请让他们在终端运行 auth0 tenants use <tenant>。
  4. 询问 API 名称和标识符 — 使用 AskUserQuestion:“你希望将 Auth0 API 命名为什么?它应使用什么标识符(audience)?例如:名称:‘我的 Go API’,标识符:‘https://my-api.example.com’。标识符是一个逻辑 URI,无需能够解析——它只用于唯一标识你的 API。” 等待回答。如果用户不确定,建议根据 go.mod 中的项目模块名生成标识符(例如 https://<module-name>)。
  5. 询问作用域 — 使用 AskUserQuestion:“你的 API 需要哪些作用域(权限)?例如:read:users、write:users、read:products。如果你还不确定,我可以先使用常见默认值,之后你可以再添加。” 等待回答。
  6. 检查是否已有 API — 运行 auth0 apis list,检查是否已存在标识符符合预期的 API。如果存在,询问用户是要复用它,还是使用不同的标识符创建新的 API。
  7. 创建 API 资源 — 使用步骤 4–5 中的名称、标识符和作用域。
  8. 处理 .env — 如果 .env 文件已存在,修改前先询问用户。不要读取现有 .env 的内容(其中可能包含密钥)。如果不存在 .env,则创建一个并写入 AUTH0_DOMAIN 和 AUTH0_AUDIENCE。
  9. 将 .env 添加到 .gitignore — 如果尚未添加。
  10. 继续代码集成 — 跳过步骤 3(已完成),直接进入步骤 4 编写中间件代码。

如果用户选择手动方式,请按照设置指南中的“手动设置”部分完成全部说明。然后继续执行下面的步骤 3。

手动创建 API 的快速参考:

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

或者在 Auth0 Dashboard → Applications → APIs 中手动创建

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

重要: Domain 中不得包含 https://。中间件会自动构造 issuer URL。

4. 配置 main.go

Agent 指令(与现有代码集成):

编写代码前,先确定你属于以下哪种情况:

  • A)为现有项目添加身份验证 — 用户已有定义了路由的 main.go。这种情况下,不要用下面的模板替换其文件。请改为:
    1. 添加必要的导入项(jwtmiddleware、jwks、validator、godotenv、net/url、os、context、strings)。
    2. 添加 CustomClaims 结构体和方法。
    3. 在 main() 顶部附近添加中间件设置代码(颁发者 URL、JWKS 提供程序、验证器、中间件)。
    4. 询问要保护哪些端点(见下文)。
    5. 使用 middleware.CheckJWT() 包装指定的处理程序。
  • B)从零开始创建新项目 — 使用下面的完整模板作为起点。

停止 — 询问要保护哪些端点:

如果用户的请求未明确指定要保护哪些端点,请询问:

“哪些端点需要身份验证?例如:

  • 除健康检查/公开端点外的所有端点 — 保护所有端点,仅开放特定的公开路由
  • 特定路由 — 告诉我哪些路由需要身份验证

另外,是否有端点需要特定的作用域/权限检查(例如 POST/DELETE 需要 write:users),还是所有端点只需有效的 JWT 即可?”

等待用户回答。如果用户说“全部”或“除健康检查外的所有端点”,则保护除 /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))
}
5. 保护端点

使用 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)))
6. 测试 API

Agent 指令: 编写代码后,验证构建是否成功:

go build ./...

如果编译失败,请诊断错误并修复。最多重复 5–6 次。

失败检查: 如果尝试修复 5–6 次后构建仍然失败,请停止并使用 AskUserQuestion 询问用户: “多次尝试修复后,构建仍然失败。你希望如何继续?”

  • 让我继续迭代修复
  • 手动修复 — 我会展示剩余的错误
  • 跳过构建验证 — 不等构建成功,继续下一步

如果错误仍然存在,每隔 5–6 次迭代后重复此检查。

Agent 指令:停止 — 构建成功后询问。

构建成功后,询问用户:

“你的 API 已设置完成,且编译成功。需要我帮你设置测试吗?

  1. 需要 — 我会帮你配置 M2M 应用以获取测试令牌。
  2. 不需要 — 我暂时就做到这里。

你希望选择哪一项?”

除非用户回答需要,否则不要继续设置测试。如果用户说不需要,请总结已完成的工作并停止。 Agent 指令(M2M 应用设置——仅在用户想要测试时):

如果用户选择设置测试,请询问:

"要测试受保护的端点,你需要一个获准为此 API 请求令牌的机器对机器(M2M)应用。

  1. 新建——我会创建一个新的 M2M 应用,并授权它访问此 API。
  2. 使用现有应用——你已经有一个 M2M 应用。请提供 Client ID,我会授权它访问此 API。

你倾向于哪种方式?(1 = 新建 / 2 = 使用现有应用)"

在用户回答前,不要继续操作。不要默默从租户中选取现有应用。

如果用户选择“新建”:

auth0 apps create \
  --name "<PROJECT_NAME> (Test App)" \
  --type m2m \
  --no-input --json

解析 JSON 以提取 client_id。不要使用 --reveal-secrets——绝不要在 Agent 上下文中暴露客户端密钥。 然后创建客户端授权:

auth0 api post "client-grants" --data '{
  "client_id": "<CLIENT_ID>",
  "audience": "<API_IDENTIFIER>",
  "scope": ["<SCOPES>"]
}'

如果用户选择“使用现有应用”: 询问 Client ID。然后创建客户端授权,使其获准访问此 API:

auth0 api post "client-grants" --data '{
  "client_id": "<USER_PROVIDED_CLIENT_ID>",
  "audience": "<API_IDENTIFIER>",
  "scope": ["<SCOPES>"]
}'

如果授权已存在(409 冲突),无需处理——该应用已获授权。 Agent 指令(令牌隔离 — 至关重要):

Agent 绝不能直接看到或显示访问令牌值。令牌安全规则:

  • 不要单独运行 auth0 test token——它会将令牌输出到标准输出
  • 不要单独运行指向 /oauth/token 端点的 curl 命令
  • 不要要求用户将令牌粘贴到对话中
  • 不要回显、打印或记录令牌值
  • 不要将令牌存储在文件中

安全测试方法(单命令链):

如果用户明确要求测试受保护的端点,Agent 可以使用单命令链,将令牌捕获到 shell 变量中并立即使用——令牌值绝不会被打印出来,也不会被 Agent 看到:

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

此方法的安全保障:

  • $(...) 会捕获标准输出——令牌被存入变量,不会显示出来
  • grep -o 只提取 JWT 模式(ey...)——不会泄露周边输出
  • echo "Token acquired (${#TEST_TOKEN} chars)" 通过仅打印长度(绝不打印令牌值)来确认成功
  • shell 变量 $TEST_TOKEN 仅在这条命令链执行期间存在——随后立即销毁
  • Agent 只能看到:"Token acquired (834 chars)" + API 响应正文(JSON)
  • 不会写入文件,不会导出环境变量,也不会留下任何持久数据

使用此模式的规则:

  1. 仅在用户明确要求测试时使用(例如:“测试一下”“运行测试”“验证端点是否正常”)
  2. 始终将令牌获取和 curl 串联在同一条 && 命令中——绝不能将它们拆分成两次 Bash 调用
  3. 要测试多个端点,请在同一条命令中串联多个 curl:
    TEST_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":"[email protected]"}' && \
    echo "" && echo "=== GET /products ===" && \
    curl -s http://localhost:8080/products -H "Authorization: Bearer $TEST_TOKEN"
    
  4. 绝不要添加 echo $TEST_TOKEN、printf $TEST_TOKEN 或任何会打印原始令牌值的命令
  5. 如果令牌获取失败(变量为空),[ -n "$TEST_TOKEN" ] 检查会中止命令链——请告知用户,M2M 应用可能尚未获得授权
  6. 必须提供 Client ID——auth0 test token 命令要求将 Client ID 作为第一个参数传入。这必须是 M2M 应用设置步骤中获取的 client_id(创建新的或使用现有的)。如果尚未完成 M2M 步骤(没有可用的 Client ID),不要尝试运行 test token 命令。请改为询问用户:“我需要一个 M2M 应用 Client ID 才能获取测试令牌。您希望我创建一个,还是您已有一个?”——然后先完成 M2M 设置。

如果用户没有要求测试,只需提供供他们手动运行的命令:

auth0 test token <CLIENT_ID> --audience <AUDIENCE> --scopes <SCOPE1,SCOPE2>
curl http://localhost:8080/<endpoint> -H "Authorization: Bearer <PASTE_TOKEN_HERE>"

完成 M2M 设置后:

  1. 在后台运行 go run . 启动服务
  2. 验证公开端点返回 200,受保护端点返回 401(无需令牌)
  3. 如果用户要求测试:使用上方安全的单命令链发起经过身份验证的请求
  4. 如果用户未要求测试:提供手动命令,并告知他们在终端中运行

测试公开端点:

curl http://localhost:8080/api/public

测试不带令牌的受保护端点(应返回 401):

curl http://localhost:8080/api/private

使用令牌测试受保护端点(安全的单命令链):

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

常见错误

错误修复方法
在 Auth0 中创建了 Application,而不是 API必须在 Auth0 Dashboard → Applications → APIs 中创建 API 资源
Audience 与 API Identifier 不匹配必须与 Auth0 Dashboard 中设置的 API Identifier 完全一致
Domain 包含 https://仅使用 your-tenant.auth0.com 格式,颁发者 URL 会自动构建
使用 v2 位置参数,而不是 v3 选项v3 使用 validator.WithKeyFunc()、validator.WithAlgorithm() 等
颁发者 URL 缺少末尾斜杠颁发者必须为末尾带斜杠的 https://domain/
RBAC 检查的是 scope 声明,而不是 permissions使用包含 Permissions []string 字段的自定义声明结构体
缺少 godotenv.Load() 调用添加 github.com/joho/godotenv,并在读取环境变量前调用 godotenv.Load()
使用 ContextKey{} 访问声明(v2 模式)改用类型安全的泛型 jwtmiddleware.GetClaims[T]()

基于 Scope 的授权

有关定义和强制执行 scope 与权限策略,请参阅集成指南。


CORS 配置

对于由基于浏览器的 SPA 调用的 API,请在任何身份验证中间件之前配置 CORS:

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

将其作为最外层处理程序包裹 mux:

handler := corsMiddleware(mux)
log.Fatal(http.ListenAndServe(":8080", handler))

有关详细的 CORS 模式,请参阅集成指南。


DPoP 支持

内置符合 RFC 9449 的持有证明令牌绑定。有关配置,请参阅集成指南。


相关技能

  • auth0-quickstart - Auth0 基本设置
  • auth0-mfa - 添加多因素身份验证

快速参考

配置选项:

  • validator.WithKeyFunc(provider.KeyFunc) - 用于签名验证的 JWKS 密钥函数(必需)
  • validator.WithAlgorithm(validator.RS256) - 预期的签名算法(必需)
  • validator.WithIssuer(url) - 末尾带斜杠的令牌颁发者 URL(必需)
  • validator.WithAudience(aud) - Auth0 API 设置中的 API Identifier(必需)
  • validator.WithCustomClaims(fn) - 自定义声明结构体的工厂函数
  • validator.WithAllowedClockSkew(d) - 时钟偏差容差

声明访问:

  • jwtmiddleware.GetClaims[*validator.ValidatedClaims](r.Context()) - 类型安全的声明检索
  • claims.RegisteredClaims.Subject - 用户 ID(sub)
  • claims.CustomClaims.(*CustomClaims).Scope - 以空格分隔的 scope
  • claims.CustomClaims.(*CustomClaims).Permissions - 权限字符串

常见用例:


详细文档

  • 设置指南 - Auth0 CLI 设置、环境配置
  • 集成指南 - Scope 策略、DPoP、框架适配器、错误处理
  • API 参考 - 完整的配置选项以及验证器/中间件参考

References

评论

支持 Markdown