hexomc_lib/auth/
microsoft.rs1use serde::{Deserialize, Serialize};
2use std::time::{SystemTime, UNIX_EPOCH};
3use tokio::time::{sleep, Duration};
4
5use crate::error::{HexoError, Result};
6
7const MS_CLIENT_ID: &str = "00000000402b5328";
9const MS_DEVICE_CODE_URL: &str =
10 "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
11const MS_TOKEN_URL: &str =
12 "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
13const XBL_URL: &str = "https://user.auth.xboxlive.com/user/authenticate";
14const XSTS_URL: &str = "https://xsts.auth.xboxlive.com/xsts/authorize";
15const MC_LOGIN_URL: &str =
16 "https://api.minecraftservices.com/authentication/login_with_xbox";
17const MC_PROFILE_URL: &str = "https://api.minecraftservices.com/minecraft/profile";
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct AuthResult {
21 pub player_name: String,
22 pub uuid: String,
23 pub access_token: String,
24 pub xuid: String,
25 pub refresh_token: String,
26 pub expires_at: u64,
28}
29
30#[derive(Debug, Clone)]
32pub struct DeviceCodeInfo {
33 pub verification_uri: String,
35 pub user_code: String,
37 pub message: String,
39}
40
41pub async fn request_device_code() -> Result<(DeviceCodeInfo, MsDeviceCodeResponse)> {
44 let client = reqwest::Client::new();
45 let resp: MsDeviceCodeResponse = client
46 .post(MS_DEVICE_CODE_URL)
47 .form(&[
48 ("client_id", MS_CLIENT_ID),
49 ("scope", "XboxLive.signin offline_access"),
50 ])
51 .send()
52 .await?
53 .json()
54 .await?;
55
56 let info = DeviceCodeInfo {
57 verification_uri: resp.verification_uri.clone(),
58 user_code: resp.user_code.clone(),
59 message: resp.message.clone(),
60 };
61
62 Ok((info, resp))
63}
64
65pub async fn poll_device_code(device_resp: &MsDeviceCodeResponse) -> Result<AuthResult> {
67 let client = reqwest::Client::new();
68 let interval = Duration::from_secs(device_resp.interval as u64);
69 let deadline =
70 SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + device_resp.expires_in as u64;
71
72 loop {
73 sleep(interval).await;
74
75 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
76 if now > deadline {
77 return Err(HexoError::AuthError("裝置代碼已過期".to_string()));
78 }
79
80 let token_resp: serde_json::Value = client
81 .post(MS_TOKEN_URL)
82 .form(&[
83 ("client_id", MS_CLIENT_ID),
84 ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
85 ("device_code", &device_resp.device_code),
86 ])
87 .send()
88 .await?
89 .json()
90 .await?;
91
92 if let Some(err) = token_resp.get("error") {
93 match err.as_str().unwrap_or("") {
94 "authorization_pending" => continue,
95 "slow_down" => {
96 sleep(Duration::from_secs(5)).await;
97 continue;
98 }
99 other => {
100 return Err(HexoError::AuthError(format!("MS token 錯誤: {}", other)));
101 }
102 }
103 }
104
105 let ms_access_token = token_resp["access_token"]
106 .as_str()
107 .ok_or_else(|| HexoError::AuthError("缺少 MS access_token".into()))?
108 .to_string();
109
110 let refresh_token = token_resp["refresh_token"]
111 .as_str()
112 .unwrap_or("")
113 .to_string();
114
115 let expires_in: u64 = token_resp["expires_in"].as_u64().unwrap_or(3600);
116 let expires_at =
117 SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + expires_in;
118
119 return complete_auth(&client, &ms_access_token, refresh_token, expires_at).await;
120 }
121}
122
123pub async fn refresh_token(refresh_token: &str) -> Result<AuthResult> {
125 let client = reqwest::Client::new();
126 let token_resp: serde_json::Value = client
127 .post(MS_TOKEN_URL)
128 .form(&[
129 ("client_id", MS_CLIENT_ID),
130 ("grant_type", "refresh_token"),
131 ("refresh_token", refresh_token),
132 ])
133 .send()
134 .await?
135 .json()
136 .await?;
137
138 if let Some(err) = token_resp.get("error") {
139 return Err(HexoError::AuthError(format!(
140 "refresh token 失敗: {}",
141 err.as_str().unwrap_or("unknown")
142 )));
143 }
144
145 let ms_access_token = token_resp["access_token"]
146 .as_str()
147 .ok_or_else(|| HexoError::AuthError("缺少 access_token".into()))?
148 .to_string();
149
150 let new_refresh = token_resp["refresh_token"]
151 .as_str()
152 .unwrap_or(refresh_token)
153 .to_string();
154
155 let expires_in: u64 = token_resp["expires_in"].as_u64().unwrap_or(3600);
156 let expires_at =
157 SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + expires_in;
158
159 complete_auth(&client, &ms_access_token, new_refresh, expires_at).await
160}
161
162async fn complete_auth(
163 client: &reqwest::Client,
164 ms_access_token: &str,
165 refresh_token: String,
166 expires_at: u64,
167) -> Result<AuthResult> {
168 let xbl_resp: serde_json::Value = client
170 .post(XBL_URL)
171 .json(&serde_json::json!({
172 "Properties": {
173 "AuthMethod": "RPS",
174 "SiteName": "user.auth.xboxlive.com",
175 "RpsTicket": format!("d={}", ms_access_token)
176 },
177 "RelyingParty": "http://auth.xboxlive.com",
178 "TokenType": "JWT"
179 }))
180 .send()
181 .await?
182 .json()
183 .await?;
184
185 let xbl_token = xbl_resp["Token"]
186 .as_str()
187 .ok_or_else(|| HexoError::AuthError("XBL token 失敗".into()))?
188 .to_string();
189
190 let user_hash = xbl_resp["DisplayClaims"]["xui"][0]["uhs"]
191 .as_str()
192 .ok_or_else(|| HexoError::AuthError("XBL uhs 失敗".into()))?
193 .to_string();
194
195 let xsts_resp: serde_json::Value = client
197 .post(XSTS_URL)
198 .json(&serde_json::json!({
199 "Properties": {
200 "SandboxId": "RETAIL",
201 "UserTokens": [xbl_token]
202 },
203 "RelyingParty": "rp://api.minecraftservices.com/",
204 "TokenType": "JWT"
205 }))
206 .send()
207 .await?
208 .json()
209 .await?;
210
211 if let Some(xerr) = xsts_resp.get("XErr") {
212 return Err(HexoError::AuthError(format!("XSTS 錯誤: {}", xerr)));
213 }
214
215 let xsts_token = xsts_resp["Token"]
216 .as_str()
217 .ok_or_else(|| HexoError::AuthError("XSTS token 失敗".into()))?
218 .to_string();
219
220 let xuid = xsts_resp["DisplayClaims"]["xui"][0]["xid"]
221 .as_str()
222 .unwrap_or("")
223 .to_string();
224
225 let mc_resp: serde_json::Value = client
227 .post(MC_LOGIN_URL)
228 .json(&serde_json::json!({
229 "identityToken": format!("XBL3.0 x={};{}", user_hash, xsts_token)
230 }))
231 .send()
232 .await?
233 .json()
234 .await?;
235
236 let mc_token = mc_resp["access_token"]
237 .as_str()
238 .ok_or_else(|| HexoError::AuthError("MC token 失敗".into()))?
239 .to_string();
240
241 let profile_resp: serde_json::Value = client
243 .get(MC_PROFILE_URL)
244 .bearer_auth(&mc_token)
245 .send()
246 .await?
247 .json()
248 .await?;
249
250 if profile_resp.get("error").is_some() {
251 return Err(HexoError::AuthError("帳號未購買 Minecraft".to_string()));
252 }
253
254 let player_name = profile_resp["name"]
255 .as_str()
256 .ok_or_else(|| HexoError::AuthError("無法取得玩家名稱".into()))?
257 .to_string();
258
259 let uuid = profile_resp["id"]
260 .as_str()
261 .ok_or_else(|| HexoError::AuthError("無法取得 UUID".into()))?
262 .to_string();
263
264 Ok(AuthResult {
265 player_name,
266 uuid,
267 access_token: mc_token,
268 xuid,
269 refresh_token,
270 expires_at,
271 })
272}
273
274#[derive(Debug, Deserialize)]
275pub struct MsDeviceCodeResponse {
276 pub device_code: String,
277 pub user_code: String,
278 pub verification_uri: String,
279 pub expires_in: u32,
280 pub interval: u32,
281 pub message: String,
282}