aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src-tauri/src/core/auth.rs
diff options
context:
space:
mode:
authorHsiangNianian <i@jyunko.cn>2026-01-13 18:22:49 +0800
committerHsiangNianian <i@jyunko.cn>2026-01-13 18:22:49 +0800
commitf49685f30fe93f105ad4bde61d639f6e0a5c2c0f (patch)
treec93d16ab29b3317eca2b75911613b3b1c1797266 /src-tauri/src/core/auth.rs
parentd821c824de70b537d78bac4f9567b81673f53e0e (diff)
downloadDropOut-f49685f30fe93f105ad4bde61d639f6e0a5c2c0f.tar.gz
DropOut-f49685f30fe93f105ad4bde61d639f6e0a5c2c0f.zip
feat: implement Microsoft account login flow and refactor account handling
Diffstat (limited to 'src-tauri/src/core/auth.rs')
-rw-r--r--src-tauri/src/core/auth.rs282
1 files changed, 279 insertions, 3 deletions
diff --git a/src-tauri/src/core/auth.rs b/src-tauri/src/core/auth.rs
index 39c4ce0..18e51ee 100644
--- a/src-tauri/src/core/auth.rs
+++ b/src-tauri/src/core/auth.rs
@@ -2,14 +2,57 @@ use serde::{Deserialize, Serialize};
use std::sync::Mutex;
use uuid::Uuid;
+// --- Account Types ---
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "type")]
+pub enum Account {
+ Offline(OfflineAccount),
+ Microsoft(MicrosoftAccount),
+}
+
+impl Account {
+ pub fn username(&self) -> String {
+ match self {
+ Account::Offline(a) => a.username.clone(),
+ Account::Microsoft(a) => a.username.clone(),
+ }
+ }
+
+ pub fn uuid(&self) -> String {
+ match self {
+ Account::Offline(a) => a.uuid.clone(),
+ Account::Microsoft(a) => a.uuid.clone(),
+ }
+ }
+
+ pub fn access_token(&self) -> String {
+ match self {
+ Account::Offline(_) => "null".to_string(),
+ Account::Microsoft(a) => a.access_token.clone(),
+ }
+ }
+}
+
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OfflineAccount {
pub username: String,
pub uuid: String,
}
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct MicrosoftAccount {
+ pub username: String,
+ pub uuid: String,
+ pub access_token: String,
+ pub refresh_token: Option<String>,
+ pub expires_at: i64,
+}
+
+// --- State ---
+
pub struct AccountState {
- pub active_account: Mutex<Option<OfflineAccount>>,
+ pub active_account: Mutex<Option<Account>>,
}
impl AccountState {
@@ -20,9 +63,242 @@ impl AccountState {
}
}
+// --- Offline Utils ---
+
pub fn generate_offline_uuid(username: &str) -> String {
- // Generate a UUID v3 (MD5-based) using the username as the name
- // This provides a consistent UUID for the same username
let namespace = Uuid::NAMESPACE_OID;
Uuid::new_v3(&namespace, username.as_bytes()).to_string()
}
+
+// --- Microsoft Auth Logic ---
+
+// Constants
+// Using a common public client ID for Minecraft Launchers
+const CLIENT_ID: &str = "00000000-402b-802d-0000-000000000000";
+const SCOPE: &str = "XboxLive.Signin offline_access openid profile email";
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct DeviceCodeResponse {
+ pub user_code: String,
+ pub device_code: String,
+ pub verification_uri: String,
+ pub expires_in: u64,
+ pub interval: u64,
+ pub message: Option<String>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct TokenResponse {
+ pub access_token: String,
+ pub refresh_token: Option<String>,
+ pub expires_in: u64,
+}
+
+// Error response from token endpoint
+#[derive(Debug, Serialize, Deserialize)]
+pub struct TokenError {
+ pub error: String,
+}
+
+// Xbox Live Auth
+#[derive(Debug, Serialize, Deserialize)]
+pub struct XboxLiveResponse {
+ #[serde(rename = "Token")]
+ pub token: String,
+ #[serde(rename = "DisplayClaims")]
+ pub display_claims: DisplayClaims,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct DisplayClaims {
+ pub xui: Vec<serde_json::Value>, // We need "uhs" from this
+}
+
+// Minecraft Auth
+#[derive(Debug, Serialize, Deserialize)]
+pub struct MinecraftAuthResponse {
+ pub access_token: String,
+ pub expires_in: u64,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct MinecraftProfile {
+ pub id: String,
+ pub name: String,
+}
+
+
+// 1. Start Device Flow
+pub async fn start_device_flow() -> Result<DeviceCodeResponse, String> {
+ let client = reqwest::Client::new();
+ let url = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
+
+ 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()
+ .await
+ .map_err(|e| e.to_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));
+ }
+
+ 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 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)
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .body(serde_urlencoded::to_string(&params).map_err(|e| e.to_string())?)
+ .send()
+ .await
+ .map_err(|e| e.to_string())?;
+
+ // 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"
+ }
+
+ 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 url = "https://user.auth.xboxlive.com/user/authenticate";
+
+ let payload = serde_json::json!({
+ "Properties": {
+ "AuthMethod": "RPS",
+ "SiteName": "user.auth.xboxlive.com",
+ "RpsTicket": format!("d={}", ms_access_token)
+ },
+ "RelyingParty": "http://auth.xboxlive.com",
+ "TokenType": "JWT"
+ });
+
+ let resp = client.post(url)
+ .json(&payload)
+ .header("Content-Type", "application/json")
+ .header("Accept", "application/json")
+ .send()
+ .await
+ .map_err(|e| e.to_string())?;
+
+ if !resp.status().is_success() {
+ return Err(format!("Xbox Live auth failed: {}", resp.status()));
+ }
+
+ 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()
+ .and_then(|x| x.get("uhs"))
+ .and_then(|s| s.as_str())
+ .ok_or("Failed to find UHS code")?
+ .to_string();
+
+ Ok((xbl_resp.token, uhs))
+}
+
+// 4. Authenticate with XSTS
+pub async fn method_xsts(xbl_token: &str) -> Result<String, String> {
+ let client = reqwest::Client::new();
+ let url = "https://xsts.auth.xboxlive.com/xsts/authorize";
+
+ let payload = serde_json::json!({
+ "Properties": {
+ "SandboxId": "RETAIL",
+ "UserTokens": [xbl_token]
+ },
+ "RelyingParty": "rp://api.minecraftservices.com/",
+ "TokenType": "JWT"
+ });
+
+ 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));
+ }
+
+ let xsts_resp: XboxLiveResponse = resp.json().await.map_err(|e| e.to_string())?;
+ Ok(xsts_resp.token)
+}
+
+// 5. Authenticate with Minecraft
+pub async fn login_minecraft(xsts_token: &str, uhs: &str) -> Result<String, String> {
+ let client = reqwest::Client::new();
+ 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)
+ .json(&payload)
+ .send()
+ .await
+ .map_err(|e| e.to_string())?;
+
+ if !resp.status().is_success() {
+ return Err(format!("Minecraft auth failed: {}", resp.status()));
+ }
+
+ let mc_resp: MinecraftAuthResponse = resp.json().await.map_err(|e| e.to_string())?;
+ Ok(mc_resp.access_token)
+}
+
+// 6. Get Profile
+pub async fn fetch_profile(mc_access_token: &str) -> Result<MinecraftProfile, String> {
+ let client = reqwest::Client::new();
+ let url = "https://api.minecraftservices.com/minecraft/profile";
+
+ let resp = client.get(url)
+ .bearer_auth(mc_access_token)
+ .send()
+ .await
+ .map_err(|e| e.to_string())?;
+
+ if !resp.status().is_success() {
+ return Err(format!("Profile fetch failed: {}", resp.status()));
+ }
+
+ let profile: MinecraftProfile = resp.json().await.map_err(|e| e.to_string())?;
+ Ok(profile)
+}