Better Auth MCP 插件实战:用 @better-auth/mcp 构建 OAuth 2.1 保护的 MCP 服务器

发布时间:2026/9/11 12:19:10
Better Auth MCP 插件实战:用 @better-auth/mcp 构建 OAuth 2.1 保护的 MCP 服务器 Better Auth MCP 插件实战用 better-auth/mcp 构建 OAuth 2.1 保护的 MCP 服务器【免费下载链接】better-authThe most comprehensive authentication framework项目地址: https://gitcode.com/GitHub_Trending/be/better-authbetter-auth/mcp是 Better Auth 官方的 Model Context ProtocolMCP插件它把 Better Auth 实例同时变成一个 OAuth 2.1 / OIDC 授权服务器和受保护资源服务器让 MCP 客户端Agent、LLM 工具调用方通过标准的授权码 PKCE 流程获取访问令牌来调用你的 MCP 工具。本文以packages/mcp/CHANGELOG.md记录的 1.7.x 版本演进为主线结合 插件源码 与 测试用例完整讲解插件的接入配置、受保护资源模型、路由保护、Scope 阶梯授权、DPoP 与刷新令牌复用等核心机制并给出从旧版迁移的完整步骤。一、better-auth/mcp 是什么mcp()将你的 Better Auth 应用转变为 OAuth 2.1 授权服务器与 MCP 受保护资源底层构建在better-auth/oauth-provider之上。它提供两个关键职责见 README作为授权服务器签发绑定到指定resource的访问令牌客户端通过授权码 PKCE、刷新令牌等标准流程获取令牌作为受保护资源服务器发布 RFC 9728 Protected Resource Metadata让符合标准的 MCP 客户端通过 OAuth Discovery 自动发现端点并完成授权。需要特别强调的是边界划分better-auth/mcp只负责授权不负责 MCP 协议传输。MCP 2026-07-28 协议请求应当使用官方modelcontextprotocol/server的 2.x 版本提供服务将createMcpHandler配置为legacy: reject挂载在requireMcpAuth之后并且只暴露 HTTPPOST路由。新版协议每个请求独立处理不再需要 Redis 之类的 MCP 会话存储多实例的subscriptions/listen部署可以共享一个 SDK 事件总线而无须引入协议层会话。包的基本信息见 packages/mcp/package.json当前版本 1.7.3依赖better-auth/oauth-provider与jose官方 MCP 2.0.0 客户端/服务端包作为开发依赖用于测试验证。二、快速上手最小可用配置来自 README 的标准接入方式如下import { betterAuth } from better-auth; import { jwt } from better-auth/plugins; import { cimd } from better-auth/cimd; import { fetchClientMetadataResource } from better-auth/cimd/node; import { mcp } from better-auth/mcp; export const auth betterAuth({ plugins: [ jwt(), mcp({ loginPage: /login, consentPage: /consent, resource: https://api.example.com/mcp, }), cimd({ fetchClientMetadataResource, metadataProfile: mcp-2026-07-28, }), ], });几个关键点jwt()插件是必需的。从 1.7.0 起令牌签名依赖jwt()插件未配置则无法签发/验证访问令牌CHANGELOG 明确要求 add thejwt()plugin, which is now required for token signing。resource选项必填见 plugin.ts它是本 MCP 服务器的规范受保护资源标识符RFC 8707 / RFC 9728签发的令牌 audience 绑定到它同时写入受保护资源元数据与resources并被用作预期的令牌 audience。cimd()组合用于 Client ID Metadata Document 流程MCP 2026-07-28 固定使用 CIMD draft-00通过显式的metadataProfile: mcp-2026-07-28钉住该草案。resource 值的合法性校验handler.ts 中的validateMcpResource对resource做了严格校验接入时需满足必须是单个 URL 字符串必须是绝对 URL不能包含用户名/密码凭据不能包含 fragment#不能包含 query?——若需保护带查询参数的资源改用verifyAccessTokenRequest验证并配合createResourceServerChallenge生成挑战协议必须是https:仅当主机为 loopbacklocalhost、[::1]、127.x.x.x时允许http:用于本地开发。对应测试见 plugin.test.tsurn:example:mcp、数组、非 loopback 的http://、127.example.com这类伪装主机名、带凭据/片段/查询的 URL 都会被拒绝https://api.example.com/mcp与各 loopback HTTP 地址则被接受。三、1.7.0 架构演进插件独立成包与端点迁移CHANGELOG 记录的 1.7.0PR #9992是本次演进的分水岭核心变化有三点MCP 插件从better-auth内核迁出成为独立包better-auth/mcp构建在better-auth/oauth-provider之上。授权插件与受保护请求辅助函数从包根导入原来内核中的 MCP 客户端createMcpAuthClient及其适配器被移除MCP 协议与传输客户端改用官方modelcontextprotocol/client、modelcontextprotocol/server的 2.x 版本。OAuth 端点从/mcp/*迁移到/oauth2/*发现文档位于/.well-known/oauth-authorization-server受保护资源元数据位于/.well-known/oauth-protected-resource。基于发现机制的 MCP 客户端会自动跟随新端点无需改动客户端配置。辅助函数重命名共享认证路由辅助withMcpAuth→requireMcpAuth独立受保护资源工厂mcpHandler→createMcpProtectedRequestHandler并改为接收一个扁平的McpProtectedRequestHandlerOptions对象。requireMcpAuth的职责见 require-mcp-auth.ts从实例上下文中解析 base URL对访问令牌按已发布的 JWKS 验证签名、issuer、audience 与过期时间对于 DPoP 绑定的令牌验证 DPoP proof然后把验证通过的访问令牌 claims 传给你的处理器。无认证请求收到 JSON-RPC 401 与 RFC 9728WWW-Authenticate头MCP 客户端据此启动授权流程。四、受保护资源模型RFC 8707 / RFC 97281.7.0 同时引入了对受保护资源的显式建模PR #9648资源可以用resources配置或通过oauthResource管理 API 创建。每个资源可以定义令牌 TTL、允许的 scope、自定义 JWT claims 与 JWT 签名 pin。validAudiences被移除原有资源标识符需要迁移到resources中。mcp()在 plugin.ts 内部做了三件事把配置的resource追加进oauthOptions.resources已存在则去重使签发的访问令牌按 RFC 8707 绑定到该资源把resource追加进clientRegistrationDefaultResources新注册的客户端默认关联到该资源在onRequest钩子中接管/.well-known/oauth-protected-resource及资源路径插入的别名如/api/auth/.well-known/oauth-protected-resource返回 RFC 9728 受保护资源元数据文档仅支持GET/HEAD其他方法返回 405见 plugin.test.ts。受保护资源元数据的内容buildResourceServerMetadataplugin.ts生成的文档包含字段说明resource配置的 MCP 资源标识符authorization_servers复用当前 provider 的 issuer因为 MCP 服务器与授权服务器是同一实例bearer_methods_supported[header]dpop_signing_alg_values_supported来自dpop.signingAlgorithms或默认的 DPoP 签名算法集dpop_bound_access_tokens_required仅当该资源配置了dpopBoundAccessTokensRequired时出现scopes_supported仅包含适用于受保护资源的 scope过滤掉openid、profile、email、phone、address、offline_access等授权服务器专用 scope测试 mcp.test.ts 验证了 SDK v2 客户端从该文档发现到resource: MCP_RESOURCE、authorization_servers: [AUTHORIZATION_SERVER]、scopes_supported: [mcp:base, greeting]的完整流程。五、保护 MCP 路由requireMcpAuth 与 createMcpProtectedRequestHandler方式一requireMcpAuth授权服务器与资源服务器同实例import { requireMcpAuth } from better-auth/mcp; import { createMcpHandler, McpServer } from modelcontextprotocol/server; const serverHandler createMcpHandler(() { const server new McpServer({ name: my-mcp, version: 1.0.0 }); server.registerTool(greet, {}, async () ({ content: [{ type: text, text: hello from protected MCP }], })); return server; }, { legacy: reject, responseMode: json }); // 只导出 POST 路由 export const POST requireMcpAuth(auth, async (request, accessTokenClaims) { return serverHandler.fetch(request, { authInfo: { token: extractToken(request), clientId: accessTokenClaims.client_id, scopes: (accessTokenClaims.scope ?? ).split( ).filter(Boolean), expiresAt: accessTokenClaims.exp, resource: new URL(https://api.example.com/mcp), extra: { accessTokenClaims }, }, }); });RequireMcpAuthOptions的完整字段见 require-mcp-auth.ts字段默认值说明resource服务端解析的 base URL访问令牌必须绑定到的受保护资源标识符issuer服务端解析的 base URL期望的令牌签发者当jwt()配置了自定义jwt.issuer时需要覆盖jwksUrl${baseURL}/jwks授权服务器 JWKS 的 URLchallengeScopes未设置时为requiredScopes在未认证的WWW-Authenticate挑战中提示客户端应申请的 scopeRFC 6750requiredScopes无访问令牌必须包含的 scope 列表缺失则返回 403 insufficient_scope挑战并逐一列出缺失项isScopeSatisfied精确成员匹配自定义的 scope 满足策略可定义层级化 scope如admin满足readdpop.proofMaxAgeSeconds由底层决定DPoP proof 最大年龄dpop.signingAlgorithms默认 DPoP 算法集允许的 DPoP 签名算法dpop.replayStore数据库适配器支撑的 store自定义 DPoP proof 重放防护存储requireMcpAuth默认使用数据库支撑的 replay storecreateDpopReplayStore(internalAdapter)因此 anti-replay 在多实例部署下依然成立只有在特殊场景下才需要覆盖replayStore。方式二createMcpProtectedRequestHandler分离部署或动态 baseURL当资源服务器与授权服务器分离运行或服务端使用动态baseURL时改用独立的受保护请求处理器显式传入验证选项handler.tsimport { createMcpProtectedRequestHandler } from better-auth/mcp; const protect createMcpProtectedRequestHandler( { issuer: https://auth.example.com, audience: https://api.example.com/mcp, jwksUrl: https://auth.example.com/jwks, requiredScopes: [mcp:base], }, async (request, accessTokenClaims) { return serverHandler.fetch(request, { authInfo }); }, );McpProtectedRequestHandlerOptions与RequireMcpAuthOptions类似额外支持jwtVerifyOptions追加 JOSE 验证约束顶层issuer/audience仍然权威与remoteVerify对不透明令牌或远程检查令牌的 introspection 设置introspectUrl、clientId、clientSecret、force、allowMissingAudience。两个辅助函数在 index.ts 中统一从包根导出。六、Scope 阶梯授权insufficient_scope 挑战CHANGELOG 重点介绍了 1.7.0 的一个关键体验改进PR #10577MCP 客户端遇到 scope 墙时会确切得知自己还缺哪些 scope。当访问令牌缺失受保护 scope 时服务器返回403并携带 RFC 6750insufficient_scope的WWW-Authenticate挑战逐一列出缺失的 scope。客户端可以把这些 scope 合并进一次授权请求而不是每个 scope 都触发一次浏览器跳转。实现路径用requiredScopes配置受保护 scoperequireMcpAuth或createMcpProtectedRequestHandler均可默认是精确成员匹配isScopeSatisfied可定义层级化策略当某个操作需要动态确定所需 scope 时抛出createInsufficientScopeErrorcreateResourceServerChallenge把该信号和已识别的令牌失败转换成安全的 RFC 6750 挑战challengeScopes仅作为未认证时的挑战提示。处理器产生的响应、普通权限拒绝、配置失败和无关的抛错值都会保持原有的状态码与身份不会被误包装。测试 mcp.test.ts 完整演示了这一流程客户端先以mcp:base调用tools/call实际需要greetingscope收到403 insufficient_scope scopegreeting挑战后SDK 自动把 scope 并集mcp:base offline_access greeting合并进下一次授权请求完成一次跳转即获得完整权限随后成功调用工具。七、DPoP 绑定令牌RFC 94491.7.0 引入 DPoPDemonstrating Proof of Possession发送者约束令牌PR #10039。客户端可以通过三种方式请求 DPoP 绑定令牌注册时声明dpop_bound_access_tokens授权请求携带dpop_jkt或目标资源配置了dpopBoundAccessTokensRequired。签发的令牌携带cnf.jkt、返回token_type: DPoP并贯穿刷新令牌轮换、introspection 与 userinfo 全流程保持绑定。资源服务器侧verifyAccessTokenRequest负责校验Authorization: DPoP方案、proof、请求目标、访问令牌哈希与 proof 重放。requireMcpAuth默认使用数据库支撑的验证存储createDpopReplayStore(internalAdapter)所以 anti-replay 在多实例间有效仅使用 secondary-storage 的部署会拒绝 DPoP 请求而不是跳过重放保护。better-auth/mcp会在受保护资源元数据中广告 DPoP 能力并验证 DPoP 绑定请求。注意两个破坏性更名裸令牌验证器verifyAccessToken更名为verifyBearerTokenbetter-auth/oauth2与oauthProviderResourceClientaction 中都是且它拒绝 DPoP 绑定令牌可能收到 DPoP 令牌的端点应改用verifyAccessTokenRequest。资源请求输入类型从AccessTokenRequestInput更名为ResourceRequestInputDPoP 算法选项统一为signingAlgorithms。需要为 access-token 与 refresh-token 表新增confirmation列DPoP 绑定客户端增加dpopBoundAccessTokens、资源增加dpopBoundAccessTokensRequired不新增专门的 replay 表proof 重放复用验证存储。八、刷新令牌复用窗口与客户端认证refreshTokenReuseInterval默认 30 秒PR #10145 引入refreshTokenReuseInterval在配置的窗口内OAuth Provider 可以对重复的刷新请求重放同一条刷新令牌响应。OAuth Provider 本身默认严格拒绝重放mcp()为每个配置的客户端把该间隔默认设为 30 秒plugin.ts这样当一次刷新因并发/重试被旋转令牌时重试的请求可以恢复另一个请求产生的响应。若要关闭重叠窗口并保持严格重放显式设置refreshTokenReuseInterval: 0。测试 plugin.test.ts 验证了在复用窗口内重放同一个已轮换的刷新令牌会得到与首次刷新完全一致的access_token、expires_at、refresh_token、token_type、scope与id_token。机密客户端的刷新令牌认证针对 GHSA-pw9m-5jxm-xr6h 的回归测试plugin.test.ts明确了机密客户端token_endpoint_auth_method: client_secret_basic的刷新规则省略 client_secret400invalid_client不签发任何令牌错误 client_secret401invalid_client通过Authorization: Basic提供正确 secret200签发新令牌客户端被禁用disabled: true401invalid_client响应头为WWW-Authenticate: Basic成功刷新必定旋转刷新令牌。九、客户端注册策略DCR 默认关闭CIMD 组合开启1.7.0 之后mcp()不再默认启用未认证的动态客户端注册DCRPR #10577 与 PR #9992 双重确认。两条可选路径组合cimd()获取 Client ID Metadata Document 流程MCP 2026-07-28 标准方式metadataProfile: mcp-2026-07-28显式开启 DCRallowDynamicClientRegistration: true与allowUnauthenticatedClientRegistration: true。测试验证plugin.test.ts未开启 DCR 时匿名注册返回 403显式开启后注册端点出现在授权服务器元数据registration_endpoint中新注册客户端自动获得resources: [explicitResource]。applicationType 与 OAuthClient 模型变更PR #10577 还重做了客户端模型applicationType取代旧的type/public字段tokenEndpointAuthMethod单独决定认证方式none为公开客户端其余均为机密客户端。OAuthClient不再有 catch-all 字符串索引自定义线上扩展需用具名交叉类型如OAuthClient YourExtensionMetadata显式建模。动态、管理端和用户管理的注册缺省application_type时默认为webClient ID Metadata Documents 保留缺省值为null。web 重定向要求非 loopback 主机必须 HTTPSnative 重定向接受声明的 HTTPS URL、精确的 HTTP loopback 主机或反域名私有用途 scheme。十、数据库迁移与升级指南从旧版升级到 1.7.0 涉及模型重构CHANGELOG 给出了明确步骤安装新依赖better-auth/mcp、better-auth/cimd以及应用所需的官方 2.x MCP 客户端/服务端包添加jwt()插件令牌签名现在必需迁移选项把原先嵌套在oidcConfig下的选项改为mcp({ ... })的扁平选项模型变化oauthApplication表更名为oauthClient新增oauthRefreshToken与oauthClientAssertion表执行迁移npx auth migrate或npx auth generate重新生成/迁移 schema。针对 1.7.0 新增字段的迁移要点新增applicationType与可空clientDiscoveryId旧web/native直接映射user-agent-based映射为NULL以便人工重新归类绝不能从public推导clientDiscoveryId只能来自已知的发现来源不能通过检查 HTTPS client ID 推断在添加新的复合唯一索引前先对现有(clientId, resourceId)链接去重然后删除旧列自定义 schema 映射的部署需手动回填机器对机器client_credentials的 scope 权限存入可空的oauthClient.clientCredentialsScopes缺失、NULL与空值都会拒绝client_credentials令牌签发仅管理端 create/update 端点可设置client_credentials_scopes赋予非空值需要clientPrivileges批准新的configure-client-credentials-scopes动作DCR、CIMD 与用户管理注册都不能设置该字段移除clientCredentialGrantDefaultScopes把现有客户端回填为[]新行默认[]审计后显式赋予每个被批准的机器 scopeDPoP 绑定字段access-token 与 refresh-token 表的confirmation列、客户端的dpopBoundAccessTokens、资源的dpopBoundAccessTokensRequired资源模型新增oauthResource、oauthClientResource与jwks表的可空alg/crv列keyPairConfigs可在一个 keyring 中供应多算法。若不迁移使用signingAlgorithm的资源将无法找到匹配的密钥受保护资源元数据由资源服务器在自己的源发布RFC 9728OAuth Provider 暴露的挑战辅助函数会把客户端引导到该元数据。十一、动态 baseURL 与一致性修复PR #9131 修复了动态baseURL下直接 API 调用与 OAuth/MCP 发现不一致的问题无法解析 base URL 或请求主机违反allowedHosts时返回清晰的APIError应用advanced.trustedProxyHeaders后再信任转发的 host/protocol并为每次直接调用刷新请求相关的 trusted origins、providers 与 cookiesloopback 开发主机仅凭头信息时推断为 HTTP同时拒绝缺少可用 URL 与头数据的请求类对象OAuth issuer、发现文档、受保护资源与 JWKS URL 均从当前请求主机生成requireMcpAuth使用解析出的 Better Auth URL 作为默认 issuer、resource 与 JWKS URL完全动态主机的资源服务器可以用createMcpProtectedRequestHandler显式传验证选项保留以Headers、元组数组或 record 形式提供的元数据响应头。十二、从源码与测试验证整体行为better-auth/mcp的行为由三层测试覆盖可作为接入时的行为契约参考mcp.test.ts官方 SDK v2 客户端的端到端流程——DCR 注册、发现元数据、授权码 PKCE 换 token、协议版本 2026-07-28 协商、insufficient_scope阶梯授权、工具调用与刷新令牌轮换同时验证了错误 issuer 的客户端凭据会被丢弃plugin.test.ts插件级行为——DCR 默认关闭/显式开启、受保护资源元数据内容与 405/HEAD 处理、resource校验、requireMcpAuth的 401/403 挑战、授权码 PKCE 全流程、刷新令牌复用窗口与机密客户端认证handler.test.ts 与 require-mcp-auth.test.ts受保护请求处理器与共享认证辅助的单元级验证。better-auth/mcp的定位是授权归我、传输归官方 SDK用mcp()提供 OAuth 2.1 授权服务器与 RFC 9728 受保护资源元数据用requireMcpAuth/createMcpProtectedRequestHandler保护路由再配合modelcontextprotocol/server2.x 提供 2026-07-28 协议服务——这一组合构成了当前 MCP 服务器接入 Better Auth 的标准路径。【免费下载链接】better-authThe most comprehensive authentication framework项目地址: https://gitcode.com/GitHub_Trending/be/better-auth创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考