Skip to main content

hexomc_lib/auth/
microsoft.rs

1use serde::{Deserialize, Serialize};
2use std::time::{SystemTime, UNIX_EPOCH};
3use tokio::time::{sleep, Duration};
4
5use crate::error::{HexoError, Result};
6
7// Microsoft Azure app (Minecraft Launcher's public client_id).
8const 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    /// Token expiry as a Unix timestamp (seconds).
27    pub expires_at: u64,
28}
29
30/// Device code details shown to the user.
31#[derive(Debug, Clone)]
32pub struct DeviceCodeInfo {
33    /// URL for the user to open.
34    pub verification_uri: String,
35    /// Code for the user to enter.
36    pub user_code: String,
37    /// Ready-to-display message.
38    pub message: String,
39}
40
41/// Step 1: request a device code (return `DeviceCodeInfo` to show the user),
42/// then call `poll_device_code()`.
43pub 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
65/// Poll for the MS token, then run the full OAuth chain to an `AuthResult`.
66pub 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
123/// Refresh the access token using a refresh token.
124pub 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    // XBL
169    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    // XSTS
196    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    // MC Token
226    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    // MC Profile
242    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}