2026/9/13 6:53:08

Bitwarden 服务器 Send 访问令牌请求校验机制解析:基于 Duende IdentityServer 的自定义 Grant 实现

Bitwarden 服务器 Send 访问令牌请求校验机制解析:基于 Duende IdentityServer 的自定义 Grant 实现 Bitwarden 服务器 Send 访问令牌请求校验机制解析基于 Duende IdentityServer 的自定义 Grant 实现【免费下载链接】serverBitwarden infrastructure/backend (API, database, Docker, etc).项目地址: https://gitcode.com/GitHub_Trending/ser/serverSend 是 Bitwarden 中用于临时分享文本或文件的安全特性。当用户或未登录的访客访问一个受保护的 Send 时客户端必须向 Identity 服务发起一次send_access扩展授权请求只有通过 src/Identity/IdentityServer/RequestValidators/SendAccess/readme.md 所描述的请求校验流程才能换取携带send_id等自定义 Claim 的访问令牌。本文以该文档为骨架结合 Bitwarden 服务器仓库GitHub_Trending/ser/server中的源码与测试完整讲解 Send 访问请求的校验模型、认证方式、请求参数与错误响应并深入剖析每个校验器的底层实现。读完本文你将掌握send_access扩展授权的完整校验链路、四种 Send 认证方式SendInaccessible/NotAuthenticated/ResourcePassword/EmailOtp的判定逻辑与区别、令牌请求的必需参数约定以及统一错误响应结构中send_access_error_type自定义字段的含义。Send Access 特性与文档背景Send 访问请求校验Send Access Request Validation解决的问题是工具Tools/客户端在访问 Send 数据时必须满足请求校验器中定义的要求。也就是说一个 Send 是否可以被访问、以何种认证方式被访问完全由 Identity 服务侧的请求校验器决定。文档强调了一个极其重要的约束SendAccessConstants中的字符串常量与 SDK 中的 Auth 模块bitwarden-authcrate协同使用任何对这些字符串值的修改都必须是有意的并且必须在 SDK 中做对应的同步修改。同时仓库中存在快照测试snapshot testing一旦字符串发生变化测试就会失败从而帮助检测对字符串常量的非预期改动。这一机制的源码依据位于 SendAccessConstants.cs其 XML 注释明确写道Most of these need to be synced with thebitwarden-authcrate in the SDK. There is snapshot testing to help ensure this.对应的快照测试位于 SendConstantsSnapshotTests.cs它逐一断言了错误类型、Token 请求参数、OTP Token 常量以及邮件主题字符串的值。架构总览校验器与依赖注入从源码结构看Send Access 校验功能分布在两层Grant 校验入口SendAccessGrantValidator.cs 实现IExtensionGrantValidator声明GrantType为send_access见 CustomGrantTypes.cs。认证方法校验器SendPasswordRequestValidator.cs 与 SendEmailOtpRequestValidator.cs二者都实现泛型接口 ISendAuthenticationMethodValidatorT其中T分别是ResourcePassword与EmailOtp。这些组件在 ServiceCollectionExtensions.cs 中完成注册第 32–33 行将两个认证方法校验器注册为AddTransient第 66 行通过.AddExtensionGrantValidatorSendAccessGrantValidator()将 Grant 校验器挂载到 IdentityServer 管线。对应地测试目录 test/Identity.Test/IdentityServer/SendAccess/ 中包含SendAccessGrantValidatorTests.cs、SendPasswordRequestValidatorTests.cs、SendEmailOtpRequestValidatorTests.cs、SendConstantsSnapshotTests.cs与SendAccessTestUtilities.cs五个测试文件为整个功能提供了完整的行为契约。自定义 Claims访问令牌中的 Send 专属声明文档指出Send 访问令牌中包含针对send_access授权类型专属的自定义 Claims。这些 Claim 的实际定义与签发位置如下Claim取值签发条件send_id被访问 Send 的GUID字符串形式总是包含在签发的访问令牌中send_email授权邮箱地址仅当 Send 要求EmailOtp认证类型时设置type固定为Send总是包含Claim 名称的源码定义位于 Claims.cspublic static class SendAccessClaims { public const string SendId send_id; public const string Email send_email; }从实现看三种成功路径NotAuthenticated、密码匹配、Email OTP 验证通过都会签发send_id与type两个 Claim其中type取值为IdentityClientType.Send即字符串Send而send_email只在 Email OTP 路径中追加见 SendEmailOtpRequestValidator.cs。成功结果统一构造为GrantValidationResult以sendId.ToString()作为subject以CustomGrantTypes.SendAccesssend_access作为authenticationMethod。此外ApiResources.cs 中将send_accessscope 关联的 Claim 类型定义为subJwtClaimTypes.Subject与send_id而 ProfileService.cs 对 Send 客户端做了特殊处理当context.Client.ClientId BitwardenClient.Send时直接保留SendAccessGrantValidator添加的既有 Claims不再叠加任何用户身份 Claims。认证方式Authentication MethodsSendAuthenticationQuerySendAuthenticationQuery.cs负责根据send_id从仓库读取 Send 记录并返回一个认证方法——这是一个判别联合discriminated union其类型定义在 SendAuthenticationTypes.cspublic abstract record SendAuthenticationMethod; public record NotAuthenticated : SendAuthenticationMethod; public record ResourcePassword(string Hash) : SendAuthenticationMethod; public record EmailOtp(string[] emails) : SendAuthenticationMethod; public record SendInaccessible : SendAuthenticationMethod;查询逻辑SendAuthenticationQuery.cs按以下顺序判定SendAuthenticationMethod method send switch { null SEND_INACCESSIBLE, var s when s.Disabled SEND_INACCESSIBLE, var s when s.AccessCount s.MaxAccessCount.GetValueOrDefault(int.MaxValue) SEND_INACCESSIBLE, var s when s.ExpirationDate.GetValueOrDefault(DateTime.MaxValue) DateTime.UtcNow SEND_INACCESSIBLE, var s when s.DeletionDate DateTime.UtcNow SEND_INACCESSIBLE, var s when s.AuthType AuthType.Email s.Emails is not null EmailOtp(s.Emails), var s when s.AuthType AuthType.Password s.Password is not null new ResourcePassword(s.Password), _ NOT_AUTHENTICATED };EmailOtp构造时会把以逗号分隔的邮箱列表拆分为数组SendAuthenticationQuery.cs。SendInaccessible—— Send 不可访问这是兜底场景Send 存在但被禁用、已过期、已过删除日期、访问次数达到上限或者send_id找不到对应的 Send 记录。上述所有情况统一返回invalid_grant错误码为send_id_invalid见 SendAccessGrantValidator.cs。从源码实现上看SendInaccessible与send_id 格式非法最终返回相同的错误码均为send_id_invalid、invalid_grant这是有意为之——目的是避免向调用方泄露该 Send 是否存在这类枚举信息。NotAuthenticated—— 无需认证当 Send 未启用任何额外认证/授权保护时直接向请求方签发访问令牌。成功结果中包含send_id与typeSend两个 Claim见 SendAccessGrantValidator.cs。ResourcePassword—— 密码保护Send 受密码保护用户必须提交正确的密码哈希才能获得访问令牌。其校验逻辑位于 SendPasswordRequestValidator.cs从请求中读取password_hash_b64若该字段缺失视为请求形状错误返回invalid_requestpassword_hash_b64_required否则调用ISendPasswordHasher.PasswordHashMatches(resourcePassword.Hash, clientHashedPassword)比对哈希不匹配则返回invalid_grantpassword_hash_b64_invalid匹配则签发令牌。底层哈希比对由 SendPasswordHasher.cs 实现内部委托给 ASP.NET Core 的IPasswordHasherSendPasswordHasherMarker对空字符串同样会返回 false且因为客户端提交的是高熵预哈希机密high-entropy, pre-hashed secret实现不关心是否触发重哈希SuccessRehashNeeded也视为匹配注释还指出 Send 最长存活 30 天。EmailOtp—— 邮箱 一次性密码Send 仅对特定邮箱的所有者开放。用户必须先提交正确的邮箱确认邮箱属于授权列表后再通过 OTP 证明邮箱所有权。OTP 会发送到该邮箱用户需要连同邮箱一起提交 OTP 才能换取访问令牌。核心逻辑位于 SendEmailOtpRequestValidator.cs流程如下读取email缺失则返回invalid_requestemail_required将邮箱Trim()并ToLowerInvariant()归一化随后对授权邮箱列表做大小写不敏感StringComparer.OrdinalIgnoreCase的包含判断——因为历史数据中可能混有大小写混合的邮箱无数据迁移读取otp若缺失则调用IOtpTokenProviderDefaultOtpTokenProviderOptions生成 OTP 并通过IMailService.SendSendEmailOtpEmailAsync发送到该邮箱邮件主题见下方常量随后返回错误响应若提供了otp则调用ValidateTokenAsync校验校验失败同样返回错误响应成功则签发包含send_id、send_email与type三个 Claim 的令牌。邮件发送的底层实现在 HandlebarsMailService.cs模板为Auth.TwoFactorEmail邮件正文中硬编码提示验证码 5 分钟内有效。需要特别说明的错误语义该类错误响应在 OAuth 标准意义上并不完全符合invalid_requestvsinvalid_grant的区分——所有与邮箱/OTP 相关的错误一律返回invalid_request即使某些场景用invalid_grant更合适。这是有意设计用于更好地防止枚举攻击防止攻击者探测邮箱是否在授权列表中。该意图在 SendEmailOtpRequestValidator.cs 的注释中有明确说明。相关常量SendAccessConstants.cspublic static class OtpToken { public const string TokenProviderName send_access; public const string Purpose email_otp; public const string TokenUniqueIdentifier {0}_{1}; // {0}send_id, {1}email } public static class OtpEmail { public const string Subject Your Bitwarden Send verification code is {0}; }OTP 的缓存查找键格式为{TokenProviderName}_{Purpose}_{TokenUniqueIdentifier}即send_access_email_otp_{send_id}_{email}由IOtpTokenProviderTOptions机制驱动接口定义见 IOtpTokenProvider.cs实现见 OtpTokenProvider.cs该机制的使用说明见 OtpTokenProvider/readme.md。Send Access 请求校验Send Access Request Validation入口send_id 解析与 Grant 分发SendAccessGrantValidator.ValidateAsync首先调用GetRequestSendId解析请求中的send_idSendAccessGrantValidator.cssend_id缺失 →send_id_required对应invalid_request描述为 send_id is required.send_id存在但无法通过 Base64URL 解码为有效 GUID或解码后为Guid.Empty→send_id_invalid对应invalid_grant描述为 send_id is invalid.。解析成功后GetAuthenticationMethod(sendId)查询出认证方法并分派switch (method) { case SendInaccessible: // invalid_grant send_id_invalid case NotAuthenticated: // 直接签发 case ResourcePassword rp: // 委托密码校验器 case EmailOtp eo: // 委托 Email OTP 校验器 default: throw new InvalidOperationException($Unknown auth method: {method.GetType()}); }Required Parameters必需参数文档规定的参数约定如下所有字段均位于令牌请求Token Request的原始参数中场景参数说明所有请求send_id被访问 Send 的Base64 URL 编码的 GUID密码保护的 Sendpassword_hash_b64客户端哈希后的 Base64 编码密码Email OTP 保护的 Sendemail与该 Send 关联的邮箱地址Email OTP 保护的 Sendotp一次性密码可选——若缺失则生成并发送 OTP其中send_id的 Base64 URL 编码/解码由CoreHelpers.Base64UrlDecode完成见 SendAccessGrantValidator.cs测试工具 SendAccessTestUtilities.cs 展示了客户端构造请求的完整形态除了上述参数外还会带上grant_typesend_access、client_idBitwardenClient.Send、scopeApiScopes.ApiSendAccess与device_type。完整请求示例结合源码与测试工具一个完整的 Email OTP 校验请求形如POST /connect/token Content-Type: application/x-www-form-urlencoded grant_typesend_access client_idbitwarden-send scopeapi.send_access device_type1 send_idBase64Url(GUID) emailaliceexample.com otp123456 // 可选若省略服务端生成并发送 OTP 邮件密码保护的请求则将email/otp替换为password_hash_b64客户端哈希并 Base64 编码的密码客户端配置SendClientBuilder静态客户端 SendClientBuilder.cs 定义了send_access授权可用的 Client 配置AllowedGrantTypes [CustomGrantTypes.SendAccess]仅允许该扩展授权AccessTokenLifetime 60 * globalSettings.SendAccessTokenLifetimeInMinutes令牌生命周期默认 5 分钟见 GlobalSettings.csAllowOfflineAccess false禁止签发刷新令牌RequireClientSecret falseSend 是公共匿名客户端无需也无法安全使用客户端密钥AllowedCorsOrigins [Vault]允许 Web Vault 使用该客户端AllowedScopes [ApiScopes.ApiSendAccess]允许请求api.send_accessscope。Error Responses错误响应所有错误响应都会额外包含一个自定义字段send_access_error_type其响应结构如下{ error: invalid_request|invalid_grant, error_description: Human readable description, send_access_error_type: specific_error_code }该字段的常量名定义于 SendAccessConstants.cspublic const string SendAccessError send_access_error_type;其用法置于GrantValidationResult.CustomResponse中在该常量的注释中有明确说明。完整的错误码清单均被 SendConstantsSnapshotTests.cs 快照锁定send_access_error_type取值对应error触发场景send_id_requiredinvalid_request请求中缺少send_id请求形状错误send_id_invalidinvalid_grantsend_id不是合法 GUID、解码为空 GUID或 Send 不存在/不可访问password_hash_b64_requiredinvalid_request密码保护的 Send 缺少password_hash_b64字段password_hash_b64_invalidinvalid_grant密码哈希不匹配请求形状正确但数据错误email_requiredinvalid_requestEmail OTP 保护的 Send 缺少email字段email_and_otp_requiredinvalid_request邮箱不在授权列表中或邮箱正确但 OTP 缺失/无效默认错误响应错误码常量分组定义于 SendAccessConstants.csSendIdGuidValidatorResultsvalid_send_guid/send_id_required/send_id_invalid其中valid_send_guid仅用于内部流转不会出现在响应中PasswordValidatorResultspassword_hash_b64_invalid/password_hash_b64_requiredEmailOtpValidatorResultsemail_required/email_and_otp_required。测试契约行为如何被验证SendAccessGrantValidatorTestsSendAccessGrantValidatorTests.cs覆盖了入口校验的每条分支缺少send_id→invalid_request且描述为 send_id is required.非法格式 / 空 GUID 的send_id→invalid_grant且描述为 send_id is invalid.SendInaccessible→invalid_grant 自定义响应send_access_error_typesend_id_invalidNotAuthenticated→ 成功subject 为 sendId、认证方式为send_accessClaims 含send_id与typeSendResourcePassword/EmailOtp→ 分别恰好调用一次对应类型的ISendAuthenticationMethodValidatorT.ValidateRequestAsync未知认证方法 → 抛出InvalidOperationException消息以 Unknown auth method: 开头。SendEmailOtpRequestValidatorTestsSendEmailOtpRequestValidatorTests.cs则验证缺少邮箱时返回invalid_request且不会触发 OTP 生成与邮件发送邮箱不在授权列表时返回 email and otp are required. 且同样不触发邮件正确邮箱且缺失 OTP 时生成并发送邮件错误 OTP 会被ValidateTokenAsync拒绝。这些测试通过 NSubstitute 对IOtpTokenProviderDefaultOtpTokenProviderOptions与IMailService进行打桩完整刻画了 Email OTP 两阶段流程的边界行为。小结从 readme 文档到源码实现Send Access 校验在 Bitwarden 服务器中的完整链路为客户端以send_access扩展授权类型发起令牌请求 →SendAccessGrantValidator解析并校验send_idBase64URL 编码的 GUID→SendAuthenticationQuery依据 Send 状态返回四种认证方法之一 → 分别走直接签发 / 密码哈希比对 / 邮箱 OTP 两阶段验证路径 → 成功后签发携带send_id、send_email仅 EmailOtp、typeSend自定义 Claim 的短期访问令牌默认 5 分钟、无刷新令牌失败则统一返回带send_access_error_type自定义字段的错误响应。这套机制既保证了公开匿名访问 Send 的安全性密码哈希、邮箱所有权证明又通过有意的错误语义设计降低了枚举攻击风险其字符串常量与 SDK 通过快照测试保持严格同步。【免费下载链接】serverBitwarden infrastructure/backend (API, database, Docker, etc).项目地址: https://gitcode.com/GitHub_Trending/ser/server创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考