diff options
Diffstat (limited to 'src-tauri/src/core/auth.rs')
| -rw-r--r-- | src-tauri/src/core/auth.rs | 282 |
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(¶ms).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(¶ms).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) +} |