summaryrefslogtreecommitdiffstatshomepage
path: root/src-tauri/src/core
diff options
context:
space:
mode:
authorHsiangNianian <i@jyunko.cn>2026-01-13 18:52:55 +0800
committerHsiangNianian <i@jyunko.cn>2026-01-13 18:52:55 +0800
commitb766fabacb544079fe415e501b8618a2d534f1f4 (patch)
treeb0ff97b05a62857184a48fd249184e234568841f /src-tauri/src/core
parent464fe3ba5242bcb16e5ae3d9941f52a3b9a5ee4b (diff)
downloadDropOut-b766fabacb544079fe415e501b8618a2d534f1f4.tar.gz
DropOut-b766fabacb544079fe415e501b8618a2d534f1f4.zip
feat: implement custom User-Agent client for Microsoft authentication and refactor request handling
Diffstat (limited to 'src-tauri/src/core')
-rw-r--r--src-tauri/src/core/auth.rs103
1 files changed, 65 insertions, 38 deletions
diff --git a/src-tauri/src/core/auth.rs b/src-tauri/src/core/auth.rs
index eb4f3a0..ede8f5a 100644
--- a/src-tauri/src/core/auth.rs
+++ b/src-tauri/src/core/auth.rs
@@ -4,6 +4,15 @@ use uuid::Uuid;
// --- Account Types ---
+// Helper to create a client with a custom User-Agent
+// This is critical because Microsoft's WAF often blocks requests without a valid UA
+fn get_client() -> reqwest::Client {
+ reqwest::Client::builder()
+ .user_agent("DropOut/1.0 (Linux)")
+ .build()
+ .unwrap_or_else(|_| get_client())
+}
+
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Account {
@@ -27,7 +36,7 @@ impl Account {
}
pub fn access_token(&self) -> String {
- match self {
+ match self {
Account::Offline(_) => "null".to_string(),
Account::Microsoft(a) => a.access_token.clone(),
}
@@ -46,7 +55,7 @@ pub struct MicrosoftAccount {
pub uuid: String,
pub access_token: String,
pub refresh_token: Option<String>,
- pub expires_at: i64,
+ pub expires_at: i64,
}
// --- State ---
@@ -73,7 +82,7 @@ pub fn generate_offline_uuid(username: &str) -> String {
// --- Microsoft Auth Logic ---
// Constants
-const CLIENT_ID: &str = "fe165602-5410-4441-92f7-326e10a7cb82";
+const CLIENT_ID: &str = "fe165602-5410-4441-92f7-326e10a7cb82";
const SCOPE: &str = "XboxLive.Signin offline_access openid profile email";
#[derive(Debug, Serialize, Deserialize)]
@@ -126,18 +135,15 @@ pub struct MinecraftProfile {
pub name: String,
}
-
// 1. Start Device Flow
pub async fn start_device_flow() -> Result<DeviceCodeResponse, String> {
- let client = reqwest::Client::new();
+ let client = get_client();
let url = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
-
- let params = [
- ("client_id", CLIENT_ID),
- ("scope", SCOPE),
- ];
- let resp = client.post(url)
+ let params = [("client_id", CLIENT_ID), ("scope", SCOPE)];
+
+ let resp = client
+ .post(url)
.header("Content-Type", "application/x-www-form-urlencoded")
.body(serde_urlencoded::to_string(&params).map_err(|e| e.to_string())?)
.send()
@@ -147,26 +153,33 @@ pub async fn start_device_flow() -> Result<DeviceCodeResponse, String> {
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_else(|_| "No body".to_string());
- return Err(format!("Device code request failed: {} - Body: {}", status, text));
+ return Err(format!(
+ "Device code request failed: {} - Body: {}",
+ status, text
+ ));
}
- let body = resp.json::<DeviceCodeResponse>().await.map_err(|e| e.to_string())?;
+ let body = resp
+ .json::<DeviceCodeResponse>()
+ .await
+ .map_err(|e| e.to_string())?;
Ok(body)
}
// 2. Poll for Token (Simplified: User calls this repeatedly or we loop inside a command)
// We'll implement a function that tries ONCE, consuming the device_code.
pub async fn exchange_code_for_token(device_code: &str) -> Result<TokenResponse, String> {
- let client = reqwest::Client::new();
+ let client = get_client();
let url = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
-
+
let params = [
("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
("client_id", CLIENT_ID),
("device_code", device_code),
];
- let resp = client.post(url)
+ let resp = client
+ .post(url)
.header("Content-Type", "application/x-www-form-urlencoded")
.body(serde_urlencoded::to_string(&params).map_err(|e| e.to_string())?)
.send()
@@ -175,12 +188,12 @@ pub async fn exchange_code_for_token(device_code: &str) -> Result<TokenResponse,
// Check application level error (e.g. "authorization_pending")
let text = resp.text().await.map_err(|e| e.to_string())?;
-
+
// Try parse success
if let Ok(token_resp) = serde_json::from_str::<TokenResponse>(&text) {
return Ok(token_resp);
}
-
+
// Try parse error
if let Ok(err_resp) = serde_json::from_str::<TokenError>(&text) {
return Err(err_resp.error); // "authorization_pending", "expired_token", "access_denied"
@@ -189,10 +202,9 @@ pub async fn exchange_code_for_token(device_code: &str) -> Result<TokenResponse,
Err(format!("Unknown response: {}", text))
}
-
// 3. Authenticate with Xbox Live
pub async fn method_xbox_live(ms_access_token: &str) -> Result<(String, String), String> {
- let client = reqwest::Client::new();
+ let client = get_client();
let url = "https://user.auth.xboxlive.com/user/authenticate";
let payload = serde_json::json!({
@@ -205,7 +217,8 @@ pub async fn method_xbox_live(ms_access_token: &str) -> Result<(String, String),
"TokenType": "JWT"
});
- let resp = client.post(url)
+ let resp = client
+ .post(url)
.json(&payload)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
@@ -220,9 +233,12 @@ pub async fn method_xbox_live(ms_access_token: &str) -> Result<(String, String),
}
let xbl_resp: XboxLiveResponse = resp.json().await.map_err(|e| e.to_string())?;
-
+
// Extract UHS (User Hash)
- let uhs = xbl_resp.display_claims.xui.first()
+ let uhs = xbl_resp
+ .display_claims
+ .xui
+ .first()
.and_then(|x| x.get("uhs"))
.and_then(|s| s.as_str())
.ok_or("Failed to find UHS code")?
@@ -233,7 +249,7 @@ pub async fn method_xbox_live(ms_access_token: &str) -> Result<(String, String),
// 4. Authenticate with XSTS
pub async fn method_xsts(xbl_token: &str) -> Result<String, String> {
- let client = reqwest::Client::new();
+ let client = get_client();
let url = "https://xsts.auth.xboxlive.com/xsts/authorize";
let payload = serde_json::json!({
@@ -245,17 +261,18 @@ pub async fn method_xsts(xbl_token: &str) -> Result<String, String> {
"TokenType": "JWT"
});
- let resp = client.post(url)
+ let resp = client
+ .post(url)
.json(&payload)
.send()
.await
.map_err(|e| e.to_string())?;
if !resp.status().is_success() {
- // Should handle specific errors like "Account not verified", "Age restriction"
- let status = resp.status();
- let text = resp.text().await.unwrap_or_default();
- return Err(format!("XSTS auth failed: {} - {}", status, text));
+ // Should handle specific errors like "Account not verified", "Age restriction"
+ let status = resp.status();
+ let text = resp.text().await.unwrap_or_default();
+ return Err(format!("XSTS auth failed: {} - {}", status, text));
}
let xsts_resp: XboxLiveResponse = resp.json().await.map_err(|e| e.to_string())?;
@@ -264,14 +281,15 @@ pub async fn method_xsts(xbl_token: &str) -> Result<String, String> {
// 5. Authenticate with Minecraft
pub async fn login_minecraft(xsts_token: &str, uhs: &str) -> Result<String, String> {
- let client = reqwest::Client::new();
+ let client = get_client();
let url = "https://api.minecraftservices.com/authentication/login_with_xbox";
let payload = serde_json::json!({
"identityToken": format!("XBL3.0 x={};{}", uhs, xsts_token)
});
- let resp = client.post(url)
+ let resp = client
+ .post(url)
.json(&payload)
.send()
.await
@@ -280,7 +298,10 @@ pub async fn login_minecraft(xsts_token: &str, uhs: &str) -> Result<String, Stri
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_else(|_| "No body".to_string());
- return Err(format!("Minecraft auth failed: {} - Body: {}", status, text));
+ return Err(format!(
+ "Minecraft auth failed: {} - Body: {}",
+ status, text
+ ));
}
let mc_resp: MinecraftAuthResponse = resp.json().await.map_err(|e| e.to_string())?;
@@ -289,10 +310,11 @@ pub async fn login_minecraft(xsts_token: &str, uhs: &str) -> Result<String, Stri
// 6. Get Profile
pub async fn fetch_profile(mc_access_token: &str) -> Result<MinecraftProfile, String> {
- let client = reqwest::Client::new();
+ let client = get_client();
let url = "https://api.minecraftservices.com/minecraft/profile";
- let resp = client.get(url)
+ let resp = client
+ .get(url)
.bearer_auth(mc_access_token)
.send()
.await
@@ -322,22 +344,27 @@ pub struct EntitlementsResponse {
}
pub async fn check_ownership(mc_access_token: &str) -> Result<bool, String> {
- let client = reqwest::Client::new();
+ let client = get_client();
let url = "https://api.minecraftservices.com/entitlements/mcstore";
- let resp = client.get(url)
+ let resp = client
+ .get(url)
.bearer_auth(mc_access_token)
.send()
.await
.map_err(|e| e.to_string())?;
if !resp.status().is_success() {
+ let status = resp.status();
let text = resp.text().await.unwrap_or_default();
- return Err(format!("Entitlement check failed: {} - {}", resp.status(), text));
+ return Err(format!("Entitlement check failed: {} - {}", status, text));
}
let body: EntitlementsResponse = resp.json().await.map_err(|e| e.to_string())?;
// We look for "product_minecraft" or "game_minecraft"
- let owns_game = body.items.iter().any(|e| e.name == "product_minecraft" || e.name == "game_minecraft");
+ let owns_game = body
+ .items
+ .iter()
+ .any(|e| e.name == "product_minecraft" || e.name == "game_minecraft");
Ok(owns_game)
}