news 2026/9/13 6:52:57

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Bitwarden 服务器 Send 访问令牌请求校验机制解析:基于 Duende IdentityServer 的自定义 Grant 实现

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

【免费下载链接】serverBitwarden infrastructure/backend (API, database, Docker, etc).项目地址: https://gitcode.com/GitHub_Trending/ser/server

Send 是 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,声明GrantTypesend_access(见 CustomGrantTypes.cs)。
  • 认证方法校验器:SendPasswordRequestValidator.cs 与 SendEmailOtpRequestValidator.cs,二者都实现泛型接口 ISendAuthenticationMethodValidator<T>,其中T分别是ResourcePasswordEmailOtp

这些组件在 ServiceCollectionExtensions.cs 中完成注册:第 32–33 行将两个认证方法校验器注册为AddTransient,第 66 行通过.AddExtensionGrantValidator<SendAccessGrantValidator>()将 Grant 校验器挂载到 IdentityServer 管线。

对应地,测试目录 test/Identity.Test/IdentityServer/SendAccess/ 中包含SendAccessGrantValidatorTests.csSendPasswordRequestValidatorTests.csSendEmailOtpRequestValidatorTests.csSendConstantsSnapshotTests.csSendAccessTestUtilities.cs五个测试文件,为整个功能提供了完整的行为契约。

自定义 Claims:访问令牌中的 Send 专属声明

文档指出,Send 访问令牌中包含针对send_access授权类型专属的自定义 Claims。这些 Claim 的实际定义与签发位置如下:

Claim取值签发条件
send_id被访问 Send 的GUID(字符串形式)总是包含在签发的访问令牌中
send_email授权邮箱地址仅当 Send 要求EmailOtp认证类型时设置
type固定为Send总是包含

Claim 名称的源码定义位于 Claims.cs:

public static class SendAccessClaims { public const string SendId = "send_id"; public const string Email = "send_email"; }

从实现看,三种成功路径(NotAuthenticated、密码匹配、Email OTP 验证通过)都会签发send_idtype两个 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 Methods)

SendAuthenticationQuery(SendAuthenticationQuery.cs)负责根据send_id从仓库读取 Send 记录,并返回一个"认证方法"——这是一个判别联合(discriminated union),其类型定义在 SendAuthenticationTypes.cs:

public 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_invalidinvalid_grant),这是有意为之——目的是避免向调用方泄露"该 Send 是否存在"这类枚举信息。

NotAuthenticated—— 无需认证

当 Send 未启用任何额外认证/授权保护时,直接向请求方签发访问令牌。成功结果中包含send_idtype=Send两个 Claim(见 SendAccessGrantValidator.cs)。

ResourcePassword—— 密码保护

Send 受密码保护,用户必须提交正确的密码哈希才能获得访问令牌。其校验逻辑位于 SendPasswordRequestValidator.cs:

  1. 从请求中读取password_hash_b64
  2. 若该字段缺失,视为请求形状错误,返回invalid_request+password_hash_b64_required
  3. 否则调用ISendPasswordHasher.PasswordHashMatches(resourcePassword.Hash, clientHashedPassword)比对哈希;
  4. 不匹配则返回invalid_grant+password_hash_b64_invalid;匹配则签发令牌。

底层哈希比对由 SendPasswordHasher.cs 实现:内部委托给 ASP.NET Core 的IPasswordHasher<SendPasswordHasherMarker>,对空字符串同样会返回 false;且因为客户端提交的是高熵预哈希机密(high-entropy, pre-hashed secret),实现不关心是否触发重哈希(SuccessRehashNeeded也视为匹配),注释还指出 Send 最长存活 30 天。

EmailOtp—— 邮箱 + 一次性密码

Send 仅对特定邮箱的所有者开放。用户必须先提交正确的邮箱;确认邮箱属于授权列表后,再通过 OTP 证明邮箱所有权。OTP 会发送到该邮箱,用户需要连同邮箱一起提交 OTP 才能换取访问令牌。核心逻辑位于 SendEmailOtpRequestValidator.cs,流程如下:

  1. 读取email,缺失则返回invalid_request+email_required
  2. 将邮箱Trim()ToLowerInvariant()归一化,随后对授权邮箱列表做大小写不敏感StringComparer.OrdinalIgnoreCase)的包含判断——因为历史数据中可能混有大小写混合的邮箱(无数据迁移);
  3. 读取otp;若缺失,则调用IOtpTokenProvider<DefaultOtpTokenProviderOptions>生成 OTP 并通过IMailService.SendSendEmailOtpEmailAsync发送到该邮箱(邮件主题见下方常量),随后返回错误响应;
  4. 若提供了otp,则调用ValidateTokenAsync校验;校验失败同样返回错误响应;成功则签发包含send_idsend_emailtype三个 Claim 的令牌。

邮件发送的底层实现在 HandlebarsMailService.cs:模板为Auth.TwoFactorEmail,邮件正文中硬编码提示验证码 5 分钟内有效。

需要特别说明的错误语义:该类错误响应在 OAuth 标准意义上并不完全符合invalid_requestvsinvalid_grant的区分——所有与邮箱/OTP 相关的错误一律返回invalid_request,即使某些场景用invalid_grant更合适。这是有意设计,用于更好地防止枚举攻击(防止攻击者探测邮箱是否在授权列表中)。该意图在 SendEmailOtpRequestValidator.cs 的注释中有明确说明。

相关常量(SendAccessConstants.cs):

public 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},由IOtpTokenProvider<TOptions>机制驱动(接口定义见 IOtpTokenProvider.cs,实现见 OtpTokenProvider.cs,该机制的使用说明见 OtpTokenProvider/readme.md)。

Send Access 请求校验(Send Access Request Validation)

入口:send_id 解析与 Grant 分发

SendAccessGrantValidator.ValidateAsync首先调用GetRequestSendId解析请求中的send_id(SendAccessGrantValidator.cs):

  • send_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_type=send_accessclient_idBitwardenClient.Send)、scopeApiScopes.ApiSendAccess)与device_type

完整请求示例

结合源码与测试工具,一个完整的 Email OTP 校验请求形如:

POST /connect/token Content-Type: application/x-www-form-urlencoded grant_type=send_access &client_id=bitwarden-send &scope=api.send_access &device_type=1 &send_id=<Base64Url(GUID)> &email=alice@example.com &otp=123456 // 可选:若省略,服务端生成并发送 OTP 邮件

密码保护的请求则将email/otp替换为:

&password_hash_b64=<客户端哈希并 Base64 编码的密码>

客户端配置:SendClientBuilder

静态客户端 SendClientBuilder.cs 定义了send_access授权可用的 Client 配置:

  • AllowedGrantTypes = [CustomGrantTypes.SendAccess]:仅允许该扩展授权;
  • AccessTokenLifetime = 60 * globalSettings.SendAccessTokenLifetimeInMinutes:令牌生命周期默认 5 分钟(见 GlobalSettings.cs);
  • AllowOfflineAccess = false禁止签发刷新令牌
  • RequireClientSecret = false:Send 是公共匿名客户端,无需(也无法安全使用)客户端密钥;
  • 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.cs:public 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.cs:

  • SendIdGuidValidatorResultsvalid_send_guid/send_id_required/send_id_invalid(其中valid_send_guid仅用于内部流转,不会出现在响应中);
  • PasswordValidatorResultspassword_hash_b64_invalid/password_hash_b64_required
  • EmailOtpValidatorResultsemail_required/email_and_otp_required

测试契约:行为如何被验证

SendAccessGrantValidatorTests(SendAccessGrantValidatorTests.cs)覆盖了入口校验的每条分支:

  • 缺少send_idinvalid_request且描述为 "send_id is required.";
  • 非法格式 / 空 GUID 的send_idinvalid_grant且描述为 "send_id is invalid.";
  • SendInaccessibleinvalid_grant+ 自定义响应send_access_error_type=send_id_invalid
  • NotAuthenticated→ 成功,subject 为 sendId、认证方式为send_access,Claims 含send_idtype=Send
  • ResourcePassword/EmailOtp→ 分别恰好调用一次对应类型的ISendAuthenticationMethodValidator<T>.ValidateRequestAsync
  • 未知认证方法 → 抛出InvalidOperationException(消息以 "Unknown auth method:" 开头)。

SendEmailOtpRequestValidatorTests(SendEmailOtpRequestValidatorTests.cs)则验证:缺少邮箱时返回invalid_request不会触发 OTP 生成与邮件发送;邮箱不在授权列表时返回 "email and otp are required." 且同样不触发邮件;正确邮箱且缺失 OTP 时生成并发送邮件;错误 OTP 会被ValidateTokenAsync拒绝。这些测试通过 NSubstitute 对IOtpTokenProvider<DefaultOtpTokenProviderOptions>IMailService进行打桩,完整刻画了 Email OTP 两阶段流程的边界行为。

小结

从 readme 文档到源码实现,Send Access 校验在 Bitwarden 服务器中的完整链路为:客户端以send_access扩展授权类型发起令牌请求 →SendAccessGrantValidator解析并校验send_id(Base64URL 编码的 GUID)→SendAuthenticationQuery依据 Send 状态返回四种认证方法之一 → 分别走"直接签发 / 密码哈希比对 / 邮箱 OTP 两阶段验证"路径 → 成功后签发携带send_idsend_email(仅 EmailOtp)、type=Send自定义 Claim 的短期访问令牌(默认 5 分钟、无刷新令牌),失败则统一返回带send_access_error_type自定义字段的错误响应。这套机制既保证了公开匿名访问 Send 的安全性(密码哈希、邮箱所有权证明),又通过有意的错误语义设计降低了枚举攻击风险,其字符串常量与 SDK 通过快照测试保持严格同步。

【免费下载链接】serverBitwarden infrastructure/backend (API, database, Docker, etc).项目地址: https://gitcode.com/GitHub_Trending/ser/server

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 6:52:17

text-to-CAD技术解析:从工程语义到STEP文件的工业落地路径

1. 什么是text-to-cad&#xff1a;不是“文字变图纸”的魔法&#xff0c;而是工程语义落地的硬核桥梁 你搜“text-to-cad”时&#xff0c;看到的大多是零散提问&#xff1a;cad下载、cad画直线显示2.1616e、solidworks导入step、cad标注卡住……这些看似琐碎的问题&#xff0c;…

作者头像 李华
网站建设 2026/9/13 6:51:55

VBA事件编程实战:Excel自动化进阶指南

1. VBA事件编程入门&#xff1a;从手动到自动的蜕变在Excel办公自动化领域&#xff0c;VBA&#xff08;Visual Basic for Applications&#xff09;一直是提升效率的利器。但很多初学者止步于录制宏和手动执行代码的阶段&#xff0c;殊不知VBA事件机制才是实现真正自动化的钥匙…

作者头像 李华
网站建设 2026/9/13 6:49:54

Maven安装与配置详解:环境变量、阿里云镜像及IDEA集成避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 6:44:25

Electrobun 调试排障:5 分钟定位构建失败与运行故障

Electrobun 调试排障&#xff1a;5 分钟定位构建失败与运行故障 【免费下载链接】electrobun Build ultra fast, tiny, and cross-platform desktop apps with Typescript. 项目地址: https://gitcode.com/GitHub_Trending/el/electrobun Electrobun 是一个用 TypeScrip…

作者头像 李华
网站建设 2026/9/13 6:43:29

在 Refine 中使用 ThemedLayout 搭建 Ant Design 管理后台布局

在 Refine 中使用 ThemedLayout 搭建 Ant Design 管理后台布局 【免费下载链接】refine A React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility. 项目地址: https://gitcode.com/GitHub_Trending/re/refine …

作者头像 李华