☰
teams-for-linux 第三方 IdP 一次性验证码(OTC)预填为何被否决:ADR-028 决策全过程解析
2026/10/14 22:18:41 网站建设 项目流程
  • 桌面应用
  • 即时通讯

【免费下载链接】teams-for-linux

Unofficial Microsoft Teams for Linux client

项目地址:https://gitcode.com/gh_mirrors/te/teams-for-linux
点击查看免费下载

本文基于 teams-for-linux 仓库的架构决策记录 ADR 028,完整剖析一次已被否决的认证增强方案:为第三方身份提供商(如 Okta)的一次性验证码(One-Time-Code,OTC)字段实现自动预填。文章不仅还原了该 ADR 的上下文、备选方案与最终结论,还结合仓库中auth.webLogin.*的实际实现(app/ssoPasswordPrefill/index.js)与相关测试(tests/unit/ssoPasswordPrefill.test.js),讲清楚「为什么密码可以预填、验证码却不行」,以及「减少凭据输入的正确方向是什么」。读者读完将掌握:Web 登录预填功能的真实能力边界、Electron 无法托管密码管理器扩展的技术根因,以及当前项目支持 FIDO2、智能卡、Intune SSO 等「有契约」认证方式的具体配置方法。

一、决策状态与背景:来自真实用户的一次请求

ADR-028 的状态是❌ Rejected(已否决),调查发生于 2026 年 8 月,尝试实现于 PR #2874。它并非凭空出现的架构空想,而是由一个真实用户诉求(issue #2869)驱动:

用户请求一个totpCommand,作为已随应用发布的auth.webLogin.passwordCommand的姊妹功能——既然密码可以从密码管理器以命令方式取出并填入登录框,那么认证器(authenticator)生成的动态验证码也应该能走同样的路径。

这个诉求的诱因很普遍:很多企业(如联合国等机构)通过策略让 Teams 会话频繁过期,用户几乎每次启动都落在微软/联合(federated)网页登录页上,不得不反复输入凭据。项目已有的auth.webLogin.passwordCommand正是为解决这一痛点而生,它允许用户配置一条 shell 命令(例如pass show teams),把密码管理器的输出直接填入密码框,应用本身不存储任何秘密。

为什么密码能预填,而验证码不行

密码侧能成立的根本原因非常朴素:input[type="password"]几乎是所有登录表单的通用事实标准。无论微软的 Entra 登录页如何变化,密码字段的类型标识基本稳定,因此基于选择器(selector)的注入在绝大多数场景都可靠。

一次性验证码字段则完全不同。它没有统一的 DOM 形态:

  • 微软 Entra 的汇聚式验证码页面使用input[name="otc"]、#idTxtBx_SAOTCC_OTC这类选择器;
  • Okta Identity Engine 则使用input[name="credentials.totp"];
  • 为了覆盖其他情况,当时的尝试还设计了auth.webLogin.totpSelector作为「逃生舱」配置项。

也就是说,实现方不得不为每家 IdP、甚至每个页面版本维护一套专用选择器。正是这种「为未知形态的 DOM 编写猜测代码」的做法,为后续的失败埋下了伏笔。

真实案例:Okta 三屏流程下预填彻底失效

ADR 记录了一个极具说服力的实证:报告人所在的组织使用 Okta,登录要依次经过三个相互独立的界面——身份识别(identify)、"Verify with your password"(密码校验)、"Enter code from Okta Verify app"(输入 Okta Verify 应用生成的验证码)。

即使在把用户的确切字段名加入内置选择器列表之后,验证码步骤依然从未被填充过。这意味着:

  • 问题不是「选择器写错了」这么简单;
  • 而是 Okta 的渲染行为、页面生命周期或事件绑定机制导致注入脚本根本没能把值写进去;
  • 对一个无法在生产环境复现、无法穷举租户定制情况的 DOM 进行逆向,维护成本不可控。

二、正式决策:不追求第三方 IdP 的 OTC 预填

基于上述事实,ADR-028 的结论非常明确:

不针对第三方身份提供商实现一次性验证码预填。auth.webLogin.user与auth.webLogin.passwordCommand继续为它们所构建的微软托管登录页提供支持。

这一决策的实质是划定能力边界:现有的 web 登录预填功能只承诺覆盖微软托管登录域(login.microsoftonline.com、login.microsoft.com、login.live.com),不对第三方 IdP 的私有标记语言负责。仓库中 app/ssoPasswordPrefill/index.js 的内置登录主机常量与 app/config/options.js 的auth.webLogin.*默认值(全部为空/关闭)都印证了这一点:预填是默认不启用、需要用户显式配置的增强功能,且其宿主范围是明确受限的。

支撑决策的配置现状(可作为对照)

在 docs-site/docs/configuration.md 中,auth.webLogin的完整配置面如下:

选项类型默认值说明
auth.webLogin.userstring""邮箱/用户名,在账户字段为空时预填。为空则禁用。
auth.webLogin.passwordCommandstring""shell 命令,其 stdout 首行被预填进密码字段。为空则禁用。
auth.webLogin.extraHostsarray[]除内置微软主机外,额外视为登录页的主机后缀。联合 IdP 的登录页不在微软域名下时添加。
auth.webLogin.autoSubmitbooleanfalse自动推进:邮箱步后点 Next、密码步后点 Sign in。
auth.webLogin.verifyMethodstring""在 "Verify your identity" MFA 页面点击标签以此文本开头的选项(如Text对应短信)。

典型config.json示例(来自 app/ssoPasswordPrefill/README.md):

{ "auth": { "webLogin": { "user": "you@example.org", "passwordCommand": "pass show work/teams", "autoSubmit": true, "verifyMethod": "Text" } } }

注意auth.webLogin.verifyMethod只做「点击 MFA 方式选择」这一件事,它不会替你输入验证码——MFA 页面选好方式后弹出的验证码仍须手动键入。这恰好与 ADR-028 的边界相呼应:预填链条覆盖到「选择验证方式」为止,验证码本身不在承诺范围内。

三、被否决的备选方案一:按租户做选择器匹配

第一个被考虑的方案是继续沿着选择器路线走下去——为每个租户/IdP 维护精确的字段选择器。ADR 给出了三条扎实的否决理由:

1. Okta 刻意禁用了自动填充

Okta 的登录组件在验证码字段上刻意设置了autocomplete="off"。okta-signin-widget 中请求支持自动填充和密码管理器集成的问题(#1119)已经挂了多年且无任何进展,这证明它是一个立场而非疏漏——对第二因素(2FA)而言,这是一个站得住脚的安全选择。任何基于浏览器自动填充能力的方案从源头就被掐断了。

2. 元素标识符每次渲染都会变化,无契约可言

  • 元素 id 是每次渲染动态生成的(报告中出现的真实 id 是input54这种随机形态);
  • 唯一稳定可用的只剩name属性,而它只是 widget 的内部字段键,不携带任何兼容性契约;
  • 每个租户都可以自定义(customise)Okta widget,标记结构随时可能漂移。

「追逐这种标记」是一场必输的跑步机(a treadmill we would lose)——今天修好的选择器,明天可能在某个租户的定制主题下再次失配。

3. 每次失配在用户眼里都是一次「回归」

这是最关键的用户体验维度:预填功能一旦存在,用户就会把它当作承诺。当某个租户页面上验证码没有被填充时,用户感知到的不是「此功能不支持这个 IdP」,而是「功能坏了」。这种不可控的「假回归」噪音,是维护方最不愿意背上的包袱。

实现层佐证:现有预填如何做安全门控

从源码可以更具体地理解「选择器方案」的脆弱性。现有密码预填的注入门控isLoginUrl(app/ssoPasswordPrefill/index.js)做了双重校验:协议必须是 HTTPS,且主机名必须命中内置或extraHosts提供的主机后缀。其单元测试(tests/unit/ssoPasswordPrefill.test.js)覆盖了:

  • 内置微软主机在 HTTPS 下匹配成功;
  • 子域(如eu.login.microsoftonline.com)匹配;
  • http://明文连接即使主机匹配也被拒绝;
  • 形似主机(login.microsoftonline.com.evil.com)被拒绝;
  • extraHosts生效但仍要求 HTTPS。

这些测试证明了现有实现把「往哪注入」作为安全边界在严加防守,而 OTC 预填若要做成,就得在「往什么字段注入」上对不可信的第三方 DOM 做出同样的承诺——这是现有架构无力提供的。

四、被否决的备选方案二:加载密码管理器浏览器扩展

选择器方案之外最「显而易见」的出路是:让真正的密码管理器去完成填充——即把 1Password、KeePassXC、Bitwarden 等桌面版的浏览器扩展装进 Electron。ADR 明确指出:Electron 无法托管这类扩展,理由环环相扣:

  1. chrome.runtime.connectNative不在支持面内:Electron 支持的chrome.*API 表面并不包含 native messaging。而 native messaging 恰恰是 1Password、KeePassXC、Bitwarden 桌面集成连接本地 vault(密码库)的通道——扩展正是靠它才能和持有秘密的桌面应用通信。
  2. 加载方式受限:Electron 只加载 unpacked(未打包)扩展,不支持.crx签名包。
  3. 存储受限:仅支持chrome.storage.local。
  4. 无 UI 概念:Electron 没有 popup(弹出面板)和 extension actions(工具栏按钮)的概念,意味着连一个让用户点击「填充」的工具栏界面都不存在。

这个「墙」不是 ADR-028 首次撞上。它同样是 #2609(关于密码管理器集成)及其 spike #2610 背后的同一堵墙。ADR 还特别指出了 allowlist 方案的无效性:

在/etc/1password/custom_allowed_browsers中把我们自己的二进制加入白名单,只能控制 1Password 应用愿意通过「扩展→应用」通道与哪些浏览器对话;如果没有能使用 native messaging 的扩展,光靠 allowlist 几乎不可能有任何效果。

事实佐证同样有力:自 #2609 提出该问题以来,没有任何人确认过自动填充快捷键能在 Electron 窗口内完成填充。

五、被否决的备选方案三:直接驱动 IdP 的 API

第三条路是绕开 DOM,直接调用身份提供商的 API 完成验证码认证:

  • Okta 的 Interaction Code grant(交互码授权):这是面向「拥有 Okta 关系」的应用设计的现代授权流,要求在每个客户的组织内完成一次应用注册(app registration)。而 teams-for-linux 只是包装 Microsoft Teams 的客户端,从不拥有也不应拥有与客户 Okta org 之间的这种关系——让每个租户都为第三方客户端注册应用,推广成本与合作成本都不可接受。
  • 经典的/api/v1/authnsessionToken 路由:已被标记为过时并处于退役流程中(superseded and being retired),把方案建立在一条正在消失的 API 上毫无意义。

这一备选方案的否决进一步夯实了总决策:任何需要「拥有 IdP 关系」的方案都不适合一个包装型客户端。

六、后果与出路:有契约的认证方法才是正解

被否决并不等于「问题不存在」。ADR 的 Consequences 部分给出了明确的建设性指引:想要更少凭据提示的用户,应当改用真正有契约(contract)支撑的认证方法——即认证流程由标准组织或协议定义、而非靠抓取页面标记实现。项目当前已经支持的三条路线分别是:

1. FIDO2 / WebAuthn 硬件安全密钥(auth.webauthn.enabled)

对应 ADR 021。WebAuthn 是 W3C 标准 API(navigator.credentials),不是被爬取的标记(scraped markup)。在 Linux 上,项目通过两层navigator.credentialsmonkey-patch,把硬件密钥仪式转交给发行版自带的fido2-tools命令行套件(fido2-cred/fido2-assert)完成,实现在 app/webauthn/index.js 与 app/webauthn/fido2Backend.js。配置方式:

{ "auth": { "webauthn": { "enabled": true } } }

要求系统装有fido2-tools(Debian/Ubuntu:sudo apt install fido2-tools;Fedora:sudo dnf install fido2-tools;Arch:sudo pacman -S libfido2)。对联邦租户而言尤其相关的是auth.webauthn.extraOrigins:当 FIDO2 提示不在微软登录域名下、而是由客户自己的 IdP 提供时,可将该 IdP 的精确 HTTPS origin(含端口)加入白名单,实现在 app/webauthn/originAllowlist.js,配置示例见 docs-site/docs/configuration.md。这正是一条「第三方 IdP 场景下依然有契约可循」的替代路径。

2. 智能卡 / PKCS#11 客户端证书(auth.clientCertificate.pinDialog.enabled)

对应 ADR 024。国家 eID 卡、PIV/CAC 卡、PIV 模式下的 YubiKey 等硬件证书是企业和政府用户的刚需。Linux 上 Chromium 无原生 PIN 提示 UI,项目通过app.setClientCertRequestPasswordHandler(实现在 app/clientCertificate/index.js)配合独立加固的 PIN 输入窗口 app/_shared/securePrompt.js 完成解锁——PIN 只存在于主进程持有的独立窗口中,绝不注入 Teams 页面。配置:

{ "auth": { "clientCertificate": { "pinDialog": { "enabled": true } } } }

该功能仅限 Linux、默认关闭,且要求用户自行完成 PKCS#11 模块注册(如modutil -dbdir sql:$HOME/.pki/nssdb -add opensc -libfile ...)。

3. Intune SSO(auth.intune.enabled)

走 Microsoft Identity Broker 的 D-Bus 通道(实现在 app/intune/index.js),是另一条契约化的单点登录路径,配置见 docs-site/docs/configuration.md。

最有效的改变往往不在这个应用里

ADR 特别强调了一个经常被忽略的事实:对多数用户而言,减少验证码输入的最有效手段是组织策略层面的事,而不是客户端的事:

  • Okta Verify push(推送确认代替输入验证码);
  • "remember this device"(记住此设备,免除重复验证);
  • FastPass(Okta 的无密码/生物识别通行证)。

这三者都直接消除验证码输入环节,且都是组织策略可以一键开启的能力。

七、给未来贡献者的遗产:从已知死胡同出发

ADR 的最后一段点明了这份否决记录的价值:

如果一个未来贡献者想为稳定的内部 IdP(in-house IdP)重新实现基于选择器的预填,这并不困难。本记录存在的意义,就是让后续工作从已知的死胡同出发,而不是重新把它们踩一遍。

这揭示了 ADR 机制的核心价值:记录被否决的路径,和记录被采纳的路径同等重要。对于「稳定的内部 IdP」——即由企业自己维护、标记结构可控、不会像 Okta 那样每次渲染生成随机 id 的身份平台——选择器式预填依然是一个完全合理的可执行方向;ADR-028 为其划清了边界并沉淀了反模式清单:

  1. 不要假设autocomplete会可用(Okta 是刻意关闭的);
  2. 不要依赖元素 id(每次渲染变化,无契约);
  3. 不要在没有「扩展→应用」native messaging 通道的 Electron 里指望密码管理器扩展;
  4. 不要在包装型客户端里引入需要租户注册的 IdP API 依赖;
  5. 每一次失配都会被用户感知为回归,选择器方案的承诺必须与维护成本一起评估。

八、总结:能力边界即产品边界

ADR-028 是一次「主动说不」的架构决策,它给 teams-for-linux 的认证体系划定了清晰的分层:

场景方案契约来源状态
微软托管登录页,免重复输入密码auth.webLogin.passwordCommand+user通用input[type=password]事实标准✅ 已实现
微软托管登录页,自动推进 + 选 MFA 方式auth.webLogin.autoSubmit/verifyMethod微软登录页 DOM✅ 已实现(best-effort)
第三方 IdP 的验证码自动填充totpCommand/totpSelector无(各家 IdP 私有标记)❌ 已否决
硬件安全密钥auth.webauthn.enabledW3C WebAuthn 标准✅ 已实现(Linux opt-in beta)
智能卡客户端证书auth.clientCertificate.pinDialog.enabledPKCS#11 / NSS✅ 已实现(Phase 1)
组织级免验证码Okta Verify push / FastPass / remember-device组织策略建议用户在租户侧开启

一句话结论:把凭据交给「有契约的标准」(WebAuthn、PKCS#11、Intune Broker)而不是交给「被爬取的 DOM 标记」,既是安全上的正确选择,也是维护成本上的必然选择。ADR-028 为整个项目(以及未来可能接入的内部 IdP 方案)留下了可复用的判断框架:任何预填/自动填充功能,只有当注入目标拥有可验证、可承诺的稳定性时,才值得实现。对于开发者而言,理解这份被否决的 ADR,等于同时理解了「为什么不这么做」和「应该怎么做」——而这正是阅读架构决策记录的最大价值。

延伸阅读:本文所依的原始记录 ADR 028 位于仓库 docs-site/docs/development/adr 目录,同目录下的 ADR 021 与 ADR 024 记录了本文提及的两条已实现替代路线;auth.webLogin的完整配置说明见 docs-site/docs/configuration.md,实现与单元测试分别见 app/ssoPasswordPrefill/index.js 与 tests/unit/ssoPasswordPrefill.test.js。

  • 桌面应用
  • 即时通讯

【免费下载链接】teams-for-linux

Unofficial Microsoft Teams for Linux client

项目地址:https://gitcode.com/gh_mirrors/te/teams-for-linux
点击查看免费下载

相关推荐

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询