fix(msa): 微软登录默认走设备码,浏览器流降级为可选加速路

官方产物上点「微软登录」必失败:GUI 只接了授权码流(随机端口回环回调),而 Azure 应用
注册当初是为 device-code 建的,重定向 URI 一项都没登记,于是授权端点当场回
`invalid_request: ... 'redirect_uri' is not valid`,浏览器里只剩一张 AAD 原始错误页,
启动器干等 5 分钟超时。设备码流本身是好的(CLI `accounts add msa` 一直能用)。

- core:新增 `preflight`(回环重定向 URI 注册预检)与 `login`(浏览器优先 + 自动回退设备码)。
  预检拿「即将使用的 redirect_uri」(端口不参与匹配,微软官方文档 *Localhost exceptions*
  原文已引进模块文档)先问一次授权端点;没登记 / 问不出来就地跳过,**一个监听口都不开**。
  预检通过才真绑端口走授权码;授权码失败(超时/被拒/绑不上)自动回退设备码并带上结构化
  原因码;用户取消(`Canceled`)原样上抛,不偷偷换流。设备码是默认,零监听口。
- presenter:`start_login` = 设备码(默认);`start_browser_login` = 浏览器优先 + 回退,
  回退原因码写进 `DeviceLoginView.browser_note`(原文只进日志)。
- i18n:三条回退文案(含「怎么修:把 http://localhost/auth 加进重定向 URI,端口不参与匹配」)。
- app-iced:登录面板主按钮改成设备码、浏览器降级为次按钮;重登也走设备码;设备码面板与失败
  面板都能画回退说明。倒计时文案改由视图模型持有(借用渲染),顺带修掉中文倒计时整行不显示。
- 守卫(都能变红):①授权端点回 invalid_request 时确实回退设备码且原因是结构化码;
  ②未登记回环 / 默认设备码路 `listeners_opened == 0`;③回环登记了仍能走授权码(不焊死加速路);
  ④用户取消不被偷偷换流;⑤client id 仍只能构建期注入(扫 `auth/microsoft/` 源码无 UUID 字面量)。
This commit is contained in:
AskaEth committed 2026-11-08 20:23:42 +08:00
1 parent 329c529bea
commit f82bd395f8
23 files changed
+1373 -131

No files matched your search

+59
View File
@@ -115,6 +115,23 @@ macro_rules! account_entries {
// ── 重新登录的引导
AccountHintAuthlibReauth => ("Enter the password again for {server}.", "请输入 {server} 的密码。"),
AccountHintOfflineNoRelogin => ("Local profiles have no server account to sign in again — create a new profile instead.", "本地档案没有可重新登录的服务器账户,请新建一个档案。"),
// ── 浏览器登录回退到设备码的说明(M178)
// 口径:这不是「登录失败」——设备码接着跑。第一句说清「已改用设备码」,
// 需要改配置的那一条**必须给可照抄的下一步**(哪个门户、加什么值、端口不参与匹配),
// 且**不许**出现 AAD 的原始报文(`invalid_request` / `AADSTS…` / JSON)——
// 用户 2026-11-08 实测撞到的就是那张原始错误页。
AccountBrowserFallbackNotRegistered => (
"This build's app registration has no loopback redirect URI, so browser sign-in cannot work; switched to a device code. To fix: add http://localhost/auth under Redirect URIs in the Azure app registration (the port is ignored), then restart the launcher.",
"这个构建的应用注册里没有登记回环重定向 URI,浏览器登录用不了,已自动改用设备码。怎么修:在 Azure 应用注册的「重定向 URI」里加上 http://localhost/auth(端口不参与匹配),然后重启启动器。"
),
AccountBrowserFallbackUnverified => (
"Could not confirm that browser sign-in is usable, so it switched to a device code.",
"没能确认浏览器登录是否可用(预检没成功),已自动改用设备码。"
),
AccountBrowserFallbackFailed => (
"Browser sign-in did not finish, so it switched to a device code. If the browser page said the redirect URI is not valid, add http://localhost/auth under Redirect URIs of the app registration.",
"浏览器登录没有完成,已自动改用设备码。如果浏览器里提示 redirect_uri 无效,请把 http://localhost/auth 加进应用注册的「重定向 URI」。"
),
} }
};
}
@@ -125,6 +142,48 @@ pub(crate) use account_entries;
mod tests {
use crate::{Catalog, Key, Lang};
/// **M178**:浏览器登录回退到设备码时那一句,必须「说清为什么 + 怎么修」,且**不许**把
/// AAD 的原始报文搬上屏(用户实测撞到的正是那张 `invalid_request ... redirect_uri` 错误页)。
///
/// 抓得住:文案退回「登录失败,完整原因在日志里」、删掉可照抄的重定向 URI、或有人把
/// AAD 原始错误码/JSON 塞进词条。
/// 抓不住:读起来顺不顺(那靠 review 与截图)。
#[test]
fn the_browser_fallback_copy_explains_itself_without_raw_aad_text() {
for &lang in Lang::ALL {
let catalog = Catalog::of(lang);
let not_registered = catalog.text(Key::AccountBrowserFallbackNotRegistered);
let unverified = catalog.text(Key::AccountBrowserFallbackUnverified);
let failed = catalog.text(Key::AccountBrowserFallbackFailed);
// 「已改用设备码」这条结论三句里都得有(用户要知道画面为什么变了)。
for copy in [not_registered, unverified, failed] {
assert!(
copy.contains("device code") || copy.contains("设备码"),
"{}: {copy}",
lang.tag()
);
// 原文不上屏:这两条是 AAD 的机器码,出现即语言/脱敏纪律失效。
assert!(!copy.contains("invalid_request"), "{}: {copy}", lang.tag());
assert!(!copy.contains("AADSTS"), "{}: {copy}", lang.tag());
assert!(!copy.contains('{'), "{}: {copy}", lang.tag());
}
// 「没登记回环」这一条必须给**可照抄**的下一步(门户 + 值 + 端口不参与匹配)。
assert!(
not_registered.contains("http://localhost/auth"),
"{not_registered}"
);
assert!(
not_registered.to_lowercase().contains("redirect uri")
|| not_registered.contains("重定向 URI"),
"{not_registered}"
);
assert!(
not_registered.to_lowercase().contains("port") || not_registered.contains("端口"),
"{not_registered}"
);
}
}
/// **M132**:缺 client id 这条是本页唯一「说清原因」的失败文案,它的纪律比别的正文更硬。
///
/// 抓得住:文案退回「登录失败 / 完整原因在日志里」、退化成「未支持」、或者只说缺 id 却
+3
View File
@@ -180,6 +180,9 @@ pub(super) const ROWS: &[(&str, &str, &str)] = &[
("AccountNoticeTitleDone", "Account updated", "账户已更新"),
("AccountHintAuthlibReauth", "Enter the password again for {server}.", "请输入 {server} 的密码。"),
("AccountHintOfflineNoRelogin", "Local profiles have no server account to sign in again — create a new profile instead.", "本地档案没有可重新登录的服务器账户,请新建一个档案。"),
("AccountBrowserFallbackNotRegistered", "This build's app registration has no loopback redirect URI, so browser sign-in cannot work; switched to a device code. To fix: add http://localhost/auth under Redirect URIs in the Azure app registration (the port is ignored), then restart the launcher.", "这个构建的应用注册里没有登记回环重定向 URI,浏览器登录用不了,已自动改用设备码。怎么修:在 Azure 应用注册的「重定向 URI」里加上 http://localhost/auth(端口不参与匹配),然后重启启动器。"),
("AccountBrowserFallbackUnverified", "Could not confirm that browser sign-in is usable, so it switched to a device code.", "没能确认浏览器登录是否可用(预检没成功),已自动改用设备码。"),
("AccountBrowserFallbackFailed", "Browser sign-in did not finish, so it switched to a device code. If the browser page said the redirect URI is not valid, add http://localhost/auth under Redirect URIs of the app registration.", "浏览器登录没有完成,已自动改用设备码。如果浏览器里提示 redirect_uri 无效,请把 http://localhost/auth 加进应用注册的「重定向 URI」。"),
("SettingsGroupUpdate", "Updates", "更新"),
("SettingsUpdateCurrent", "Current version", "当前版本"),
("SettingsUpdateChannel", "Channel", "更新渠道"),