scl_core/auth/microsoft/
mod.rs1use std::fmt::Display;
4
5use serde::Deserialize;
6
7use super::structs::AuthMethod;
8use crate::{password::Password, prelude::*};
9pub mod leagcy;
10use leagcy::*;
11
12pub struct MicrosoftOAuth<T> {
19 client_id: T,
20}
21
22impl<T: Display> MicrosoftOAuth<T> {
23 pub const fn new(client_id: T) -> Self {
25 Self { client_id }
26 }
27
28 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 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 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 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 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#[derive(Debug, Clone, Deserialize, Default)]
242#[serde(default)]
243pub struct DeviceCodeResponse {
244 pub device_code: String,
246 pub user_code: String,
248 pub verification_uri: String,
250 pub expires_in: usize,
252 pub interval: usize,
254 pub message: String,
257 pub error: String,
259}
260
261#[derive(Debug, Clone, Deserialize, Default)]
265#[serde(default)]
266pub struct TokenResponse {
267 pub token_type: String,
269 pub scope: String,
271 pub expires_in: usize,
273 pub access_token: Password,
275 pub id_token: String,
277 pub refresh_token: String,
279 pub error: String,
281}