feat(core): 加 MSA 授权码流(本地回调服务器)并接到账户页
research/08 方案 A:GUI 的微软登录改走授权码流(HMCL 主流做法),设备码流保留给 CLI/无浏览器场景。两条流只差「怎么拿到 MS access_token」,拿到之后共用 chain::complete_chain 与 MicrosoftSession,落盘/刷新语义没有第二套。 core(crates/core/src/auth/microsoft/) - endpoints:新增 authorize(…/consumers/oauth2/v2.0/authorize),与 token 同租户。 - authcode:RedirectUri 是 http://localhost:{port}/auth 的唯一来源(授权 URL、 token 请求体、实际监听三处共用,坑 5);AuthCodePolicy(回调超时 300s / 绑定重试 3 次 / 固定端口 / 固定 state);authorization_url(response_type=code + prompt=select_account + scope=XboxLive.signin%20offline_access);exchange_code 复用 flow 的 oauth_error/tokens_from。 - callback:本地 HTTP/1.1 回调服务器。内核分配端口(占用按次数退避重试);非 /auth 路径 (favicon、预连)404 后继续等;state 不匹配 400 后继续等;error=access_denied 归 Canceled; 单条连接读头超时 5s,半开连接拖不垮时间预算。 - MicrosoftAuth::authcode_login(present):绑端口 → present(宿主开浏览器)→ 等回调 → 换 token → 整链;失败与设备码流一样落 M38 失败日志。 app(crates/app) - AppCommand::StartBrowserLogin + host::accounts::start_browser_login;设备码/授权码共用 LoginJob 取消槽位、收尾入库(store_session)与面板 DTO。 - host::opener 用系统默认浏览器打开授权 URL(Windows rundll32 url.dll,FileProtocolHandler)。 - 账户页:主按钮「微软登录」走授权码流,「设备码登录」作备用;DeviceLoginView::browser_waiting 在面板里给出可复制的授权链接(user_code 留空)。 测试 - crates/core/tests/auth/microsoft_authcode.rs:授权 URL 向量(租户/编码/prompt)、 URI 一致性(授权 URL 与 token 表单同字面量)、mock 全链、favicon+陈旧 state 后仍拿到 code、 端口占用重试文案、超时、access_denied→Canceled、login_with_xbox 403 的 M38 诊断 (detail_code=login_with_xbox_403,真机白名单未批时验收止步于此)。 - 单测:authcode 编码/向量、callback query 解码与错误分流、DTO browser_waiting、conv 面板。 未做/未验证:真机完整登录需 Azure 公共客户端 id(ULL_MSA_CLIENT_ID)与 Minecraft API 许可, 本机只能到「授权页 302 到 login.live.com 且 redirect_uri/scope/prompt 原样保留」这一步。 AskaEth <i@yeij.top>
This commit is contained in:
1 parent
f62bb00494
commit
8194e38bcf
22 files changed
+1188
-80
No files matched your search
@@ -1,25 +1,35 @@
|
||||
//! 微软轨:device-code 全流程 + 会话/刷新语义(`PlanDoc/07-accounts.md` §1、`analyse/04` §4–§5)。
|
||||
//! 微软轨:device-code 与授权码(浏览器)两条登录流 + 会话/刷新语义
|
||||
//! (`PlanDoc/07-accounts.md` §1、`analyse/04` §4–§5、`research/08` §3)。
|
||||
//!
|
||||
//! 分四块,各管一件事:
|
||||
//! 分五块,各管一件事:
|
||||
//! - [`session`]:`MicrosoftSession`(凭据载荷)与**刷新判定**(纯函数,可脱离网络单测);
|
||||
//! - [`endpoints`]:端点表(生产 = 微软消费版直连;测试 = 整表指向 mock);
|
||||
//! - [`flow`]:device-code 发起 / 轮询 / refresh(OAuth 层);
|
||||
//! - [`authcode`] + [`callback`]:授权码流(授权 URL + 本地 HTTP 回调服务器 + code 换 token);
|
||||
//! - [`chain`]:XBL → XSTS → login_with_xbox → entitlements → profile(Xbox/MC 层)。
|
||||
//!
|
||||
//! 两条流只差「怎么拿到 MS access_token」:拿到之后都进同一个 [`chain::complete_chain`],
|
||||
//! 落盘/刷新也共用 [`MicrosoftSession`],所以 GUI 账户页的两颗按钮不会长出两套凭据语义。
|
||||
//!
|
||||
//! # client id 从哪来
|
||||
//!
|
||||
//! device-code 流必须带一个**已在 Azure 注册的公共客户端 id**。HMCL 把它烘进 jar 的
|
||||
//! 两条流都必须带一个**已在 Azure 注册的公共客户端 id**(且租户是 `consumers`、
|
||||
//! 重定向 URI 登记为 `http://localhost`)。HMCL 把它烘进 jar 的
|
||||
//! manifest(`hmcl.microsoft.auth.id`),我们同理:二进制里不硬编码,改由
|
||||
//! `ULL_MSA_CLIENT_ID`(运行期)或构建期 `option_env!` 提供。没有配时
|
||||
//! [`MicrosoftAuth::from_env`] 返回 [`AuthError::Unsupported`] 并说清要配什么——
|
||||
//! 这比编一个假 id 让用户看到 `invalid_client` 诚实。
|
||||
|
||||
pub mod authcode;
|
||||
pub mod callback;
|
||||
pub mod chain;
|
||||
pub mod endpoints;
|
||||
pub mod flow;
|
||||
|
||||
pub mod session;
|
||||
|
||||
pub use authcode::{AuthCodePolicy, AuthCodePrompt, RedirectUri};
|
||||
pub use callback::CallbackServer;
|
||||
pub use endpoints::MicrosoftEndpoints;
|
||||
pub use flow::{DeviceCodePrompt, PollPolicy};
|
||||
pub use session::{MicrosoftSession, RefreshDecision, RefreshReason, USER_TYPE_MSA};
|
||||
@@ -45,6 +55,7 @@ pub struct MicrosoftAuth {
|
||||
client_id: String,
|
||||
endpoints: MicrosoftEndpoints,
|
||||
poll: PollPolicy,
|
||||
authcode: AuthCodePolicy,
|
||||
journal: Option<AuthJournal>,
|
||||
}
|
||||
|
||||
@@ -62,6 +73,7 @@ impl MicrosoftAuth {
|
||||
client_id: client_id.into(),
|
||||
endpoints: MicrosoftEndpoints::consumers(),
|
||||
poll: PollPolicy::default(),
|
||||
authcode: AuthCodePolicy::default(),
|
||||
journal: None,
|
||||
})
|
||||
}
|
||||
@@ -131,6 +143,13 @@ impl MicrosoftAuth {
|
||||
self
|
||||
}
|
||||
|
||||
/// 换授权码流参数(回调超时 / 固定端口 / 绑定重试 / 固定 state;测试与 CLI 用)。
|
||||
#[must_use]
|
||||
pub fn with_authcode_policy(mut self, policy: AuthCodePolicy) -> Self {
|
||||
self.authcode = policy;
|
||||
self
|
||||
}
|
||||
|
||||
/// 当前端点表。
|
||||
#[must_use]
|
||||
pub fn endpoints(&self) -> &MicrosoftEndpoints {
|
||||
@@ -177,6 +196,34 @@ impl MicrosoftAuth {
|
||||
chain::complete_chain(&self.client, &self.endpoints, tokens, now_ms()).await
|
||||
}
|
||||
|
||||
/// 授权码流登录(浏览器 + 本地回调,`research/08` §3 方案 A,GUI 的默认入口)。
|
||||
///
|
||||
/// `present` 在端口绑定之后、等待回调之前被调用:宿主在这一刻拉起系统默认浏览器
|
||||
/// (core 不做 UI,也不假设有 GUI——CLI 可以只把 URL 打到 stderr)。
|
||||
/// 失败时与 [`Self::device_code_login`] 一样往失败日志追加一条结构化记录(M38)。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 端口绑不上/网络失败、等回调超时、用户在授权页取消、OAuth 拒绝,以及整链的
|
||||
/// [`AuthError`](含白名单未批时 `login_with_xbox` 的 403 诊断)。
|
||||
pub async fn authcode_login<F>(&self, present: F) -> AuthResult<MicrosoftSession>
|
||||
where
|
||||
F: FnOnce(&AuthCodePrompt),
|
||||
{
|
||||
let result = authcode::login(
|
||||
&self.client,
|
||||
&self.endpoints,
|
||||
&self.client_id,
|
||||
self.authcode.clone(),
|
||||
present,
|
||||
)
|
||||
.await;
|
||||
if let Err(error) = &result {
|
||||
self.record_failure("authcode_login", error);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// 刷新:Live refresh → 整链重跑(**原样覆盖 refresh_token**)。
|
||||
///
|
||||
/// # Errors
|
||||
|
||||
Reference in new issue
Block a user