- 桌面应用
- 即时通讯
【免费下载链接】teams-for-linux
Unofficial Microsoft Teams for Linux client
本文基于 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.user | string | "" | 邮箱/用户名,在账户字段为空时预填。为空则禁用。 |
auth.webLogin.passwordCommand | string | "" | shell 命令,其 stdout 首行被预填进密码字段。为空则禁用。 |
auth.webLogin.extraHosts | array | [] | 除内置微软主机外,额外视为登录页的主机后缀。联合 IdP 的登录页不在微软域名下时添加。 |
auth.webLogin.autoSubmit | boolean | false | 自动推进:邮箱步后点 Next、密码步后点 Sign in。 |
auth.webLogin.verifyMethod | string | "" | 在 "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 无法托管这类扩展,理由环环相扣:
chrome.runtime.connectNative不在支持面内:Electron 支持的chrome.*API 表面并不包含 native messaging。而 native messaging 恰恰是 1Password、KeePassXC、Bitwarden 桌面集成连接本地 vault(密码库)的通道——扩展正是靠它才能和持有秘密的桌面应用通信。- 加载方式受限:Electron 只加载 unpacked(未打包)扩展,不支持
.crx签名包。 - 存储受限:仅支持
chrome.storage.local。 - 无 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 为其划清了边界并沉淀了反模式清单:
- 不要假设
autocomplete会可用(Okta 是刻意关闭的); - 不要依赖元素 id(每次渲染变化,无契约);
- 不要在没有「扩展→应用」native messaging 通道的 Electron 里指望密码管理器扩展;
- 不要在包装型客户端里引入需要租户注册的 IdP API 依赖;
- 每一次失配都会被用户感知为回归,选择器方案的承诺必须与维护成本一起评估。
八、总结:能力边界即产品边界
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.enabled | W3C WebAuthn 标准 | ✅ 已实现(Linux opt-in beta) |
| 智能卡客户端证书 | auth.clientCertificate.pinDialog.enabled | PKCS#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
相关推荐
teams-for-linux 性能审计:10 项性能敏感点的决策归档与源码验证(ADR-026)
teams for linux 性能审计:10 项性能敏感点的决策归档与源码验证(ADR 026) 导读 本文基于 teams for linux 仓库中的架构
桌面应用即时通讯teams-for-linux 屏幕共享架构决策实录:为何拒绝 Electron 38 的 useSystemPicker(ADR-008 深度解析)
teams for linux 屏幕共享架构决策实录:为何拒绝 Electron 38 的 useSystemPicker(ADR 008 深度解析) 本文以
桌面应用即时通讯teams-for-linux 为什么在 Wayland 会话中默认坚持 `--ozone-platform=x11`:ADR 031 决策全文解析
teams for linux 为什么在 Wayland 会话中默认坚持 ozone platform=x11 :ADR 031 决策全文解析 导读 本文基于
桌面应用即时通讯
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考