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>
133 lines
5.5 KiB
Rust
133 lines
5.5 KiB
Rust
//! 微软端点表:**直连**(不走镜像,承 mission Context 与 `analyse/04` §4 的端点族区分)。
|
||
//!
|
||
//! 两条流**共用同一个端点族**(`research/08` §2 硬性约束 1:租户只能用 `consumers`):
|
||
//! - 设备码流:`devicecode` + `token`;
|
||
//! - 授权码流:`authorize`(浏览器打开)+ 同一个 `token`(`grant_type=authorization_code`)。
|
||
//!
|
||
//! 都走 `login.microsoftonline.com/consumers/oauth2/v2.0/*`:用 `common` 或 AAD 租户 id
|
||
//! 拿 `XboxLive.signin` 会直接被拒(`analyse/04` §4 的「第一个坑」)。
|
||
//!
|
||
//! 端点集中成一张表是为了**测试可替换**:整表指向一个进程内 mock 服务器即可覆盖全链,
|
||
//! 生产默认走 [`MicrosoftEndpoints::consumers`]。
|
||
|
||
/// 微软账户 OAuth 与 Xbox / Minecraft 服务端点。
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub struct MicrosoftEndpoints {
|
||
/// 设备码申请(`POST`,表单 `client_id` + `scope`)。
|
||
pub device_code: String,
|
||
/// 授权码流的授权页(`GET`,浏览器打开;`research/08` §3 方案 A)。
|
||
pub authorize: String,
|
||
/// 换 token / 刷新 token / 授权码换 token(`POST`,表单)。
|
||
pub token: String,
|
||
/// Xbox Live 用户认证(`POST` JSON)。
|
||
pub xbl_authenticate: String,
|
||
/// XSTS 授权(`POST` JSON;无 Xbox 档案时回 **401** 带 `XErr`)。
|
||
pub xsts_authorize: String,
|
||
/// `login_with_xbox`(`POST` JSON,`identityToken = "XBL3.0 x=<uhs>;<xsts>"`)。
|
||
pub login_with_xbox: String,
|
||
/// 拥有权校验 `entitlements/mcstore`(`GET`,必须 200)。
|
||
pub entitlements_mcstore: String,
|
||
/// 拥有权细分 `entitlements/license`(`GET`,`/minecraft/profile` 404 时复判用)。
|
||
pub entitlements_license: String,
|
||
/// 档案 `minecraft/profile`(`GET`,带 Bearer)。
|
||
pub profile: String,
|
||
}
|
||
|
||
impl MicrosoftEndpoints {
|
||
/// 生产端点(微软消费版账户,直连)。
|
||
#[must_use]
|
||
pub fn consumers() -> Self {
|
||
Self {
|
||
device_code: "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"
|
||
.to_string(),
|
||
authorize: "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"
|
||
.to_string(),
|
||
token: "https://login.microsoftonline.com/consumers/oauth2/v2.0/token".to_string(),
|
||
xbl_authenticate: "https://user.auth.xboxlive.com/user/authenticate".to_string(),
|
||
xsts_authorize: "https://xsts.auth.xboxlive.com/xsts/authorize".to_string(),
|
||
login_with_xbox: "https://api.minecraftservices.com/authentication/login_with_xbox"
|
||
.to_string(),
|
||
entitlements_mcstore: "https://api.minecraftservices.com/entitlements/mcstore"
|
||
.to_string(),
|
||
entitlements_license: "https://api.minecraftservices.com/entitlements/license"
|
||
.to_string(),
|
||
profile: "https://api.minecraftservices.com/minecraft/profile".to_string(),
|
||
}
|
||
}
|
||
|
||
/// 把整表指向同一个基址(测试用;各端点用自己的路径后缀区分)。
|
||
#[must_use]
|
||
pub fn all_on(base: &str) -> Self {
|
||
let join = |path: &str| format!("{}/{path}", base.trim_end_matches('/'));
|
||
Self {
|
||
device_code: join("devicecode"),
|
||
authorize: join("authorize"),
|
||
token: join("token"),
|
||
xbl_authenticate: join("xbl"),
|
||
xsts_authorize: join("xsts"),
|
||
login_with_xbox: join("login_with_xbox"),
|
||
entitlements_mcstore: join("entitlements/mcstore"),
|
||
entitlements_license: join("entitlements/license"),
|
||
profile: join("profile"),
|
||
}
|
||
}
|
||
|
||
/// 所有端点(调试/诊断用)。
|
||
#[must_use]
|
||
pub fn all(&self) -> [&str; 9] {
|
||
[
|
||
&self.device_code,
|
||
&self.authorize,
|
||
&self.token,
|
||
&self.xbl_authenticate,
|
||
&self.xsts_authorize,
|
||
&self.login_with_xbox,
|
||
&self.entitlements_mcstore,
|
||
&self.entitlements_license,
|
||
&self.profile,
|
||
]
|
||
}
|
||
}
|
||
|
||
impl Default for MicrosoftEndpoints {
|
||
fn default() -> Self {
|
||
Self::consumers()
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::MicrosoftEndpoints;
|
||
|
||
#[test]
|
||
fn consumers_endpoints_stay_on_two_families() {
|
||
let endpoints = MicrosoftEndpoints::consumers();
|
||
assert!(endpoints.device_code.contains("login.microsoftonline.com"));
|
||
assert!(endpoints.token.contains("/consumers/oauth2/v2.0/token"));
|
||
// 授权码流的授权页与设备码流同族同租户(`research/08` §2:只能用 consumers)。
|
||
assert!(endpoints
|
||
.authorize
|
||
.contains("/consumers/oauth2/v2.0/authorize"));
|
||
assert!(endpoints
|
||
.xbl_authenticate
|
||
.contains("user.auth.xboxlive.com"));
|
||
assert!(endpoints.xsts_authorize.contains("xsts.auth.xboxlive.com"));
|
||
// 微软 OAuth 两流都不走 HMCL 的 login.live.com 端点族。
|
||
assert!(endpoints
|
||
.all()
|
||
.iter()
|
||
.all(|url| !url.contains("login.live.com")));
|
||
}
|
||
|
||
#[test]
|
||
fn all_on_keeps_paths_distinct() {
|
||
let endpoints = MicrosoftEndpoints::all_on("http://127.0.0.1:1234/");
|
||
assert_eq!(endpoints.device_code, "http://127.0.0.1:1234/devicecode");
|
||
assert_eq!(endpoints.authorize, "http://127.0.0.1:1234/authorize");
|
||
assert_eq!(
|
||
endpoints.entitlements_license,
|
||
"http://127.0.0.1:1234/entitlements/license"
|
||
);
|
||
}
|
||
}
|