scl_core/auth/microsoft/
mod.rs

1//! 微软登录模块,通过设备码方式获取玩家的 Microsoft 账户令牌,进而获取 Minecraft 用户令牌
2
3use std::fmt::Display;
4
5use serde::Deserialize;
6
7use super::structs::AuthMethod;
8use crate::{password::Password, prelude::*};
9pub mod leagcy;
10use leagcy::*;
11
12/// 使用设备流方式验证的微软账户验证对象
13///
14/// 使用这个对象前,你需要通过 Azure Active Directory
15/// 注册一个应用,并将其客户端 ID 提供至此使用。
16///
17/// 具体请查阅 <https://wiki.vg/Microsoft_Authentication_Scheme>
18pub struct MicrosoftOAuth<T> {
19    client_id: T,
20}
21
22impl<T: Display> MicrosoftOAuth<T> {
23    /// 通过客户端 ID 创建一个新的验证对象
24    pub const fn new(client_id: T) -> Self {
25        Self { client_id }
26    }
27
28    /// 获取一个设备码,将其展示给用户以完成浏览器验证
29    pub async fn get_devicecode(&self) -> DynResult<DeviceCodeResponse> {
30        let res = crate::http::post(
31            "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode?mkt=zh-CN",
32        )
33        .body_string(format!(
34            "client_id={}&scope=XboxLive.signin%20offline_access",
35            self.client_id
36        ))
37        .content_type("application/x-www-form-urlencoded")
38        .recv_json::<DeviceCodeResponse>()
39        .await
40        .map_err(|err| anyhow::anyhow!("请求设备码时发生错误:{}", err))?;
41
42        Ok(res)
43    }
44
45    /// 获取/验证设备码的验证情况
46    pub async fn verify_device_code(&self, device_code: &str) -> DynResult<TokenResponse> {
47        let res =
48            crate::http::post("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")
49                .body_string(format!(
50            "grant_type=urn:ietf:params:oauth:grant-type:device_code&client_id={}&device_code={}",
51            self.client_id, device_code,
52        ))
53                .content_type("application/x-www-form-urlencoded")
54                .recv_json::<TokenResponse>()
55                .await
56                .map_err(|err| anyhow::anyhow!("请求设备码时发生错误:{}", err))?;
57
58        Ok(res)
59    }
60
61    /// 重新刷新令牌,获取新的访问令牌和刷新令牌
62    async fn refresh_token(&self, refresh_token: &str) -> DynResult<TokenResponse> {
63        let res =
64            crate::http::post("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")
65                .body_string(format!(
66                    "grant_type=refresh_token&client_id={}&refresh_token={}",
67                    self.client_id, refresh_token,
68                ))
69                .content_type("application/x-www-form-urlencoded")
70                .recv_json::<TokenResponse>()
71                .await
72                .map_err(|err| anyhow::anyhow!("请求设备码时发生错误:{}", err))?;
73
74        Ok(res)
75    }
76
77    async fn auth_xbox_live(&self, access_token: &str) -> DynResult<(String, String)> {
78        println!("正在验证 Xbox Live 账户");
79        let xbox_auth_body = format!(
80            "{\
81            {\
82                \"Properties\":{\
83                    {\
84                        \"AuthMethod\":\"RPS\",\
85                        \"SiteName\":\"user.auth.xboxlive.com\",\
86                        \"RpsTicket\":\"d={access_token}\"\
87                    }\
88                },\
89                \"RelyingParty\":\"http://auth.xboxlive.com\",\
90                \"TokenType\":\"JWT\"\
91            }\
92        }"
93        );
94        let xbox_auth_resp: XBoxAuthResponse =
95            crate::http::post("https://user.auth.xboxlive.com/user/authenticate")
96                .header("Content-Type", "application/json")
97                .header("Accept", "application/json")
98                .body(xbox_auth_body.as_bytes())
99                .recv_json()
100                .await
101                .map_err(|e| anyhow::anyhow!("验证 Xbox Live 账户失败:{}", e))?;
102        let token = xbox_auth_resp.token.to_owned();
103        if let Some(uhs) = xbox_auth_resp.display_claims.xui.first() {
104            let uhs = uhs.uhs.to_owned();
105            let xsts_body = format!(
106                "{\
107                {\
108                    \"Properties\":{\
109                        {\
110                            \"SandboxId\":\"RETAIL\",\
111                            \"UserTokens\":[\"{token}\"]\
112                        }\
113                    },\
114                    \"RelyingParty\":\"rp://api.minecraftservices.com/\",\
115                    \"TokenType\":\"JWT\"\
116                }\
117            }"
118            );
119            println!("正在获取 XSTS");
120            let xsts_resp: XBoxAuthResponse =
121                crate::http::post("https://xsts.auth.xboxlive.com/xsts/authorize")
122                    .header("Content-Type", "application/json")
123                    .header("Accept", "application/json")
124                    .body(xsts_body.as_bytes())
125                    .recv_json()
126                    .await
127                    .map_err(|e| anyhow::anyhow!("获取 XSTS 账户失败:{}", e))?;
128            let xsts_token = xsts_resp.token;
129            Ok((uhs, xsts_token))
130        } else {
131            anyhow::bail!("获取 UserHash 失败")
132        }
133    }
134
135    /// 通过设备码验证获取到的 Microsoft 访问令牌获取 Minecraft 账户
136    pub async fn start_auth(
137        &self,
138        access_token: &str,
139        refresh_token: &str,
140    ) -> DynResult<AuthMethod> {
141        let (uhs, xsts_token) = self.auth_xbox_live(access_token).await?;
142
143        println!("正在获取 XUID");
144        let xuid = leagcy::get_xuid(&uhs, &xsts_token).await?;
145
146        println!("正在获取 Mojang 访问令牌");
147        let access_token = leagcy::get_mojang_access_token(&uhs, &xsts_token).await?;
148
149        if access_token.is_empty() {
150            anyhow::bail!("获取令牌失败")
151        } else {
152            println!("正在检查是否拥有 Minecraft");
153            let mcstore_resp: MinecraftStoreResponse =
154                crate::http::get("https://api.minecraftservices.com/entitlements/mcstore")
155                    .header("Authorization", &format!("Bearer {}", &access_token))
156                    .await
157                    .map_err(|e| anyhow::anyhow!(e))?
158                    .body_json()
159                    .await
160                    .map_err(|e| anyhow::anyhow!(e))?;
161            if mcstore_resp.items.is_empty() {
162                anyhow::bail!(
163                    "没有在已购项目中找到 Minecraft!请检查你的账户是否已购买 Minecraft!"
164                );
165            }
166            println!("正在获取 Minecraft 账户信息");
167            let profile_resp: MinecraftXBoxProfileResponse =
168                crate::http::get("https://api.minecraftservices.com/minecraft/profile")
169                    .header("Authorization", &format!("Bearer {}", &access_token))
170                    .await
171                    .map_err(|e| anyhow::anyhow!(e))?
172                    .body_json()
173                    .await
174                    .map_err(|e| anyhow::anyhow!(e))?;
175            if profile_resp.error.is_empty() {
176                if let Some(skin) = profile_resp.skins.iter().find(|a| a.state == "ACTIVE") {
177                    println!("正在解析皮肤");
178                    let skin_data = crate::http::get(&skin.url)
179                        .await
180                        .map_err(|e| anyhow::anyhow!(e))?
181                        .body_bytes()
182                        .await
183                        .map_err(|e| anyhow::anyhow!(e))?;
184                    let (head_skin, hat_skin) = crate::auth::parse_head_skin(skin_data)?;
185                    println!("微软账户验证成功!");
186                    Ok(AuthMethod::Microsoft {
187                        access_token,
188                        refresh_token: refresh_token.to_string().into(),
189                        xuid,
190                        head_skin,
191                        hat_skin,
192                        player_name: profile_resp.name,
193                        uuid: profile_resp.id,
194                    })
195                } else {
196                    anyhow::bail!("皮肤获取失败!");
197                }
198            } else {
199                anyhow::bail!(
200                    "没有在账户中找到 Minecraft 账户信息!请检查你的账户是否已购买 Minecraft!"
201                );
202            }
203        }
204    }
205
206    /// 刷新登录令牌,如刷新成功则可将更新后的用户继续用于正版启动
207    pub async fn refresh_auth(&self, method: &mut AuthMethod) -> DynResult {
208        if let AuthMethod::Microsoft {
209            access_token,
210            refresh_token,
211            ..
212        } = method
213        {
214            println!("正在刷新令牌");
215            let new_token = self.refresh_token(refresh_token.as_str()).await?;
216
217            *refresh_token = new_token.refresh_token.into();
218
219            let (uhs, xsts_token) = self.auth_xbox_live(&new_token.access_token).await?;
220
221            println!("正在获取 Mojang 访问令牌");
222            let new_access_token = leagcy::get_mojang_access_token(&uhs, &xsts_token).await?;
223
224            anyhow::ensure!(
225                !new_access_token.is_empty(),
226                "刷新令牌失败: {}",
227                new_access_token
228            );
229
230            *access_token = new_access_token;
231            Ok(())
232        } else {
233            anyhow::bail!("不支持的方法");
234        }
235    }
236}
237
238/// 请求设备码的响应结构
239///
240/// 关于此结构的详情可以查阅 [Microsoft 标识平台和 OAuth 2.0 设备权限授予流 - 设备授权请求](https://learn.microsoft.com/zh-cn/azure/active-directory/develop/v2-oauth2-device-code#device-authorization-request)
241#[derive(Debug, Clone, Deserialize, Default)]
242#[serde(default)]
243pub struct DeviceCodeResponse {
244    /// 一个长字符串,用于验证客户端与授权服务器之间的会话。 客户端使用此参数从授权服务器请求访问令牌。
245    pub device_code: String,
246    /// 向用户显示的短字符串,用于标识辅助设备上的会话。
247    pub user_code: String,
248    /// 用户在登录时应使用 `user_code` 转到的 URI。
249    pub verification_uri: String,
250    /// `device_code` 和 `user_code` 过期之前的秒数。
251    pub expires_in: usize,
252    /// 在发出下一个轮询请求之前客户端应等待的秒数。
253    pub interval: usize,
254    /// 用户可读的字符串,包含面向用户的说明。
255    /// 可以通过在请求中包含 `?mkt=xx-XX` 格式的查询参数并填充相应的语言区域性代码,将此字符串本地化。
256    pub message: String,
257    /// 错误信息,如果请求正常则此处是空字符串
258    pub error: String,
259}
260
261/// 请求设备码身份验证的响应结构
262///
263/// 关于此结构的详情可以查阅 [Microsoft 标识平台和 OAuth 2.0 设备权限授予流 - 成功的身份验证响应](https://learn.microsoft.com/zh-cn/azure/active-directory/develop/v2-oauth2-device-code#successful-authentication-response)
264#[derive(Debug, Clone, Deserialize, Default)]
265#[serde(default)]
266pub struct TokenResponse {
267    /// 总是为 `Bearer`。
268    pub token_type: String,
269    /// 如果返回访问令牌,则会列出该访问令牌的有效范围。
270    pub scope: String,
271    /// 包含的访问令牌有效的秒数。
272    pub expires_in: usize,
273    /// 针对请求的范围颁发。
274    pub access_token: Password,
275    /// 如果原始 `scope` 参数包含 `openid` 范围,则颁发。
276    pub id_token: String,
277    /// 如果原始 `scope` 参数包含 `offline_access`,则颁发。
278    pub refresh_token: String,
279    /// 错误信息,如果请求正常则此处是空字符串
280    pub error: String,
281}