Better Auth CIMD 插件深度解析:基于 Client ID Metadata Document draft-02 的无密钥 OAuth 客户端发现

📅 发布时间:2026/9/11 6:36:00
Better Auth CIMD 插件深度解析:基于 Client ID Metadata Document draft-02 的无密钥 OAuth 客户端发现
Better Auth CIMD 插件深度解析基于 Client ID Metadata Document draft-02 的无密钥 OAuth 客户端发现【免费下载链接】better-authThe most comprehensive authentication framework项目地址: https://gitcode.com/GitHub_Trending/be/better-auth本文围绕 better-auth 仓库中better-auth/cimd插件CHANGELOG.md展开系统讲解 Client ID Metadata Document draft-02 规范在 OAuth Provider 中的落地实现以 HTTPS URL 作为client_id的无认证客户端发现、完整的元数据校验边界、共享缓存新鲜度模型、请求放大防护以及数据模型迁移。读完本文你将掌握该插件的全部配置项、底层校验与抓取流程并能据此在自有部署中正确集成、加固与升级 CIMD 能力。CIMD 是什么让 client_id 本身成为可验证的元数据地址better-auth/cimd是 better-auth 在 1.7.0 引入的插件实现了 IETF Client ID Metadata Document draft-02。其核心思想是一个精确的 HTTPS 元数据文档 URL 就是 OAuth 的client_id客户端不再需要在授权服务器上预先注册或携带密钥授权服务器按需抓取并校验该 URL 指向的 JSON 文档从而完成无认证unauthenticated的客户端发现。在 MCPModel Context Protocol场景下这一能力尤其重要MCP 客户端是去中心化的无法与授权服务器共享密钥。插件通过显式的metadataProfile: mcp-2026-07-28模式应用 MCP 2026-07-28 规范所固定的 draft-00 元数据要求要求client_id、client_name、redirect_uris必须齐全。从 源码入口 可以看到插件整体结构export const cimd (options: CimdOptions) { const discovery createCimdClientDiscovery(options); return { id: cimd, version: PACKAGE_VERSION, init(ctx) { extendOAuthProvider(ctx, { clientDiscovery: discovery }); }, } satisfies BetterAuthPlugin; };插件通过extendOAuthProvider将构建好的ClientDiscovery注入 OAuth Provider并在 OAuth 发现文档中宣告client_id_metadata_document_supported: true见 createCimdClientDiscovery。除了cimd()插件外还提供了createCimdClientDiscovery供偏好显式组合的用户通过oauthProvider({ extensions: [{ clientDiscovery }] })使用。安装与最小集成在 package.json 中该包版本为1.7.3同时导出主入口better-auth/cimd与 Node.js 专属子路径better-auth/cimd/nodenpm install better-auth/cimd最小集成需要同时安装better-auth/oauth-provider插件通过extendOAuthProvider扩展它并与cimd()组合使用。参考 集成测试 中的真实用法import { oauthProvider } from better-auth/oauth-provider; import { cimd } from better-auth/cimd; const auth betterAuth({ plugins: [ oauthProvider({ loginPage: /login, consentPage: /consent, // CIMD 提供无认证发现DCR 相关开关可独立控制 allowDynamicClientRegistration: false, allowUnauthenticatedClientRegistration: false, scopes: [openid, profile, email, offline_access], }), cimd({ fetchClientMetadataResource: (input, init) globalThis.fetch(input, init), }), ], });需要注意的版本行为mcp()插件不再自动启用无认证动态客户端注册需要与cimd()组合以提供 Client ID Metadata Documents或显式开启两个 DCR 标志见 CHANGELOG.md。CimdOptions 完整配置项全部配置定义在 types.ts下表为关键项配置项类型默认值说明fetchClientMetadataResourceClientMetadataResourceFetch必填无默认部署方提供的抓取传输层用于元数据文档与发现归属的jwks_uri等资源metadataProfilemcp-2026-07-28无通用 draft-02附加协议画像MCP 画像强制要求client_name与redirect_urismetadataRevalidationIntervalnumber \| string60m元数据最大/兜底缓存新鲜度接受秒数或时长字符串如1dmetadataFetchPolicyCimdMetadataFetchPolicy见下文有界的请求放大防护策略maxCacheEntriesnumber1000实例内最多保留的已校验文档数LRU 淘汰originBoundFieldsreadonly string[][post_logout_redirect_uris, client_uri]URL 值必须与client_id同源的字段传空数组可关闭isMetadataDocumentUrlAllowed回调始终放行抓取前的策略闸门origin 白名单、外部信任服务等onClientCreated回调无客户端首次创建后的尽力通知onClientRefreshed回调无客户端刷新后的尽力通知fetchClientMetadataResource部署方必须提供的安全传输这是唯一必填项。其契约要求types.ts 与 CHANGELOG.md主机名只解析一次拒绝 RFC 6890 特殊用途地址将批准地址**固定pin**到连接上拒绝重定向。文档明确说明这些保证无法通过包装标准 Fetch API在 DNS 解析之后实现必须由应用在各运行时网络边界提供。测试中常见的globalThis.fetch写法仅适用于测试环境cimd.test.ts生产环境应使用下面的 Node 传输或等价实现。元数据文档校验规则从 URL 到 JSON 的完整防线校验分两层均在 validate-metadata-document.ts 中实现。client_id URL 校验draft-02 §3isCimdClientIdUrlCandidate仅做路由判定协议必须是https:不做 DNS 解析真正的安全闸门是validateClientIdUrl原始 URL 不得包含点段/./、/../含%2e编码形式不得包含 fragment#必须使用显式 HTTPS authority 形式且必须包含显式路径组件如https://client.example.com/metadata.json不得包含用户名/密码凭据主机必须是公网可路由地址loopback、私网、链路本地、云元数据地址、IPv6 隧道等一律拒绝。文档内容校验draft-02 §4.1validateCimdMetadata对抓取到的 JSON 依次执行整体结构必须是 JSON 对象通过共享的oauthClientMetadataSchemastrip()后解析校验client_id一致性文档内的client_id必须与抓取 URL 精确相等禁用字段任何被识别的密钥client secret、私钥 JWK 材料、特权字段、服务端控制字段如backchannel_logout_uri、backchannel_logout_session_required都会直接致命未知的 draft-02 成员则被忽略且永不持久化CHANGELOG.md认证方式仅允许none与private_key_jwtclient_secret_post、client_secret_basic、client_secret_jwt等对称密钥方式被禁止private_key_jwt必须同时提供jwks或jwks_uriJWKS 边界所有注册、发现、远程抓取的 JWKS 统一经过公钥非对称密钥校验——必须是{ keys: [...] }形式的 RFC 7517 JWK Set裸数组形式jwks: [key]已移除须改为jwks: { keys: [key] }EC 密钥仅限 P-256/P-384/P-521OKP 仅限 Ed25519声明的alg必须与密钥类型和曲线匹配CHANGELOG.md重定向 URIMCP 画像下redirect_uris必填且必须全部为绝对 HTTP(S) 或私有用途 URInative 私有用途重定向要求 RFC 8252 单斜杠形式如com.example.app:/callbackHTTP loopback 只接受精确的localhost、127.0.0.1、[::1]其他127.0.0.0/8地址与 localhost 子域一律拒绝CHANGELOG.md同源约束originBoundFields指定的字段值必须与client_idURL 同源redirect_uris默认被排除在同源检查之外授权时仍强制精确匹配因为 native 与分布式客户端常用与文档源不同的重定向源。校验通过后返回的元数据默认将token_endpoint_auth_method补为none并附带 URL 结构相关的warnings如根路径/、带查询串等SHOULD NOT级提示经 logger 记录。Node.js 安全传输resolve-once 与连接固定Node.js 部署可从better-auth/cimd/node导入现成实现fetchClientMetadataResourcenode.ts其内部机制与文档契约一一对应使用node:dns/promises的lookup(hostname, { all: true })一次性解析所有地址任一地址非公网可路由即抛错取第一个通过校验的地址作为pinnedAddress通过自定义lookup回调强制连接仅使用该固定地址设置agent: false不使用全局 HTTPS 连接池原始 hostname 仍作为 HTTP Host、TLS SNI 与证书校验身份保证 Host 与证书身份一致重定向响应原样返回给调用方绝不自动跟随响应体以流形式Readable.toWeb返回而不缓冲仅支持 GET 与 HEAD且要求 HTTPS URL。1.7.3 的 Patch 修复了在受支持的 Node.js 版本上客户端元数据发现偶发ERR_INVALID_IP_ADDRESS的问题CHANGELOG.md这正是该传输层连接固定逻辑的兼容性修复。非 Node 运行时需自行提供等价的解析一次 拒绝特殊地址 连接固定 拒绝重定向安全传输。缓存、新鲜度与条件重验证CIMD 的缓存遵循 HTTP 共享缓存语义并在新鲜度判定模糊时失败关闭fail closedCHANGELOG.md。核心逻辑在 resolver.ts 的computeExpiresAt指令优先级s-maxage优先于max-age与Expiress-maxage0被尊重立即过期不可缓存Cache-Control: no-store、private、Vary: *直接判定为不可缓存元数据与验证器都不会进入治理器governor新鲜度计算综合Age、Date计算当前年龄current age再与源服务器寿命、运营方metadataRevalidationInterval取较小者作为过期时刻条件重验证缓存条目记录 ETag 与 Last-Modified过期后在下次解析时携带If-None-Match/If-Modified-Since重新验证client-store.ts收到 304 但本地没有已校验缓存条目、或没有任何条件验证器时直接拒绝无条件的 304 被拒绝已过期条目在刷新前仍作为新鲜度来源合并头信息mergeCacheHeaders无后台刷新插件不做周期性或后台抓取只在客户端被解析时按需重验证。缓存实例是每个插件闭包独立的createCimdResolver的注释明确不同cimd()实例绝不共享信任状态或条件验证器并支持 LRU 淘汰maxCacheEntries上限见 resolver.ts。此外相同client_id的并发解析会被合并coalesce为同一个进行中的 Promise避免重复抓取并发刷新收敛到同一个 client-resource 链接而非在其唯一约束上失败。请求放大防护metadataFetchPolicy 治理器CIMD 通过metadataFetchPolicy对元数据抓取做有界限制types.ts默认值定义在 resolver.ts策略项默认值语义minimumFetchInterval1秒同一client_id两次抓取的最小间隔缓存命中与加入在途抓取不消耗0表示禁用maximumConcurrentFetches16全局最大在途抓取数maximumConcurrentFetchesPerOrigin4单个 URL origin 的最大在途抓取数maximumFetchesPerMinute120全局 60 秒滚动窗口内最大抓取启动数maximumFetchesPerOriginPerMinute30单 origin 60 秒滚动窗口内最大抓取启动数治理规则resolver.ts抓取启动时消耗并发与滚动窗口预算缓存命中不消耗任何预算同客户端并发解析合并共享同一次抓取超过任何限制时立即以429temporarily_unavailable拒绝而不是排队滚动 60 秒窗口按启动时间戳数组维护超窗条目被修剪这从整体上封堵了通过大量不同client_id指向同一服务来放大请求unique-client spray的攻击面数值与时长字符串通过parseDurationMs统一解析为毫秒minimumFetchInterval支持1s等 JWT 时长语法非法值抛BetterAuthError。数据模型变化与数据库迁移1.7.0 破坏性变更1.7.0 对 OAuth 客户端数据模型做了多项调整升级前必须执行迁移CHANGELOG.md新增applicationType列动态、管理端与用户托管注册在省略application_type时默认webClient ID Metadata Documents 中省略时保留为null旧值web/native直接映射user-agent-based映射为NULL待人工重新分类绝不从旧的public字段推导新增可空clientDiscoveryId列记录客户端所属的发现来源CIMD 场景值为cimd只能从已知的发现来源设置绝不允许通过检查 HTTPS client_id 推断(clientId, resourceId)链接需先去重再建复合唯一索引随后删除旧列客户端认证方式由tokenEndpointAuthMethod单独决定none为 public其余均为 confidentialOAuthClient上的旧type与public字段被移除自定义线协议扩展须用显式命名交叉类型如OAuthClient YourExtensionMetadata建模。机器对机器作用域授权独立化oauthClient.clientCredentialsScopes单独存储client_credentials授权作用域缺失、NULL或空值一律拒绝签发client_credentials令牌。仅管理端 create/update 端点暴露client_credentials_scopes且赋非空值需要clientPrivileges批准新的configure-client-credentials-scopes动作DCR、CIMD、用户托管注册均不能赋值该字段CIMD 刷新会保留管理员已拥有的值。迁移时需移除clientCredentialGrantDefaultScopes存量客户端回填为[]新行默认[]审计后在管理员显式批准后逐项赋权。生命周期事件与尽力通知插件暴露两个具名事件types.tsCimdClientCreatedEventclient新持久化的客户端、clientMetadataDocument产生该客户端的已校验文档、context发现请求的端点上下文CimdClientRefreshedEvent额外提供previousClient刷新前的客户端快照便于变更检测日志与派生字段更新。回调为尽力通知被拒绝reject的回调仅记录错误日志不回滚一次原本有效的注册/刷新client-store.ts。插件不抓取也不渲染元数据中声明的远程资源如logo_uri。此外刷新/创建会保留自定义模型名、资源链接与管理端控制的客户端标志OAuth Provider 侧还暴露clientDiscovery扩展点供自定义的可信客户端解析插件使用其稳定的id被持久化为客户端来源CHANGELOG.md。抓取管线与大小/超时边界一次完整的解析流程resolver.ts 与 client-store.ts为isCimdClientIdUrlCandidate路由判定 → 不匹配直接返回null读缓存未过期则直接返回既有客户端或按缓存文档落库命中在途解析则合并等待acquireMetadataFetchPermit获取治理许可可能 429fetchClientMetadataDocumentURL 校验 →isMetadataDocumentUrlAllowed策略闸门 → 携带Accept: application/json与条件验证头发起抓取响应约束禁止重定向、非 200 拒绝、Content-Type必须为 JSONapplication/json或application/*json、响应体上限5 KB流式读取限长超限即拒绝、5 秒超时AbortController 中止解析 JSON →validateCimdMetadata校验失败返回invalid_client警告记录日志persistMetadataDocumentClient经registerClientMetadataDocument落库并触发生命周期回调依据响应缓存头计算新鲜度并写回缓存不可缓存则删除旧条目。升级到 1.7.x重命名清单与行为变更从预发布版本升级时需要按 CHANGELOG.md 完成重命名无兼容回退旧名称新名称createCimdResolver/cimdClientDiscoverycreateCimdClientDiscoveryClientIdMetadataDocumentResultCimdMetadataValidationResultValidateCimdMetadataOptionsCimdMetadataValidationOptionsisUrlClientIdisCimdClientIdUrlCandidateMetadataDocumentFetchClientMetadataResourceFetchrefreshRatemetadataRevalidationInterval数值均为秒生命周期回调改为具名事件从metadata读取改为clientMetadataDocument从ctx改为context。CimdOptions变为必填fetchClientMetadataResource强制并移除了预发布的allowFetch、fetchMetadataDocument、allowLoopback选项采用 CIMD 时应移除allowUnauthenticatedClientRegistration除非授权服务器有意将动态客户端注册作为独立回退方案。相关实现细节可继续查阅 validate-metadata-document.ts、client-store.ts 与 node.ts行为回归覆盖见 cache.test.ts、fetch-governor.test.ts 与 mcp-e2e.test.ts。【免费下载链接】better-authThe most comprehensive authentication framework项目地址: https://gitcode.com/GitHub_Trending/be/better-auth创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考