use std::{ io::{Read, Write}, net::{SocketAddr, TcpListener, TcpStream}, }; use serde_json::json; use thiserror::Error; use crate::{constants::{self, CLIENT_ID}, utils::http::create_client, PolyError, PolyResult}; use super::{Account, AuthenticationError, AuthenticationMethod}; // TODO: store auth keys with https://beta.tauri.app/features/stronghold/ // TODO: store auth key public in prisma for easy access #[derive(Debug, Error)] pub enum MicrosoftAuthError { #[error("failed to get Minecraft access token")] MSAccessToken, #[error("failed to get XSTS token")] XSTSToken, #[error("failed to get XBL token")] XBLToken, #[error("failed to get XBL user hash")] XBLUserHash, #[error("this account doesn't have an xbox account, sign in through minecraft.net and try again")] NoXBoxAccount, #[error("this account is from a country where xbox live is not available")] XBoxLiveBlocked, #[error("this account needs adult verification on the xbox page")] AdultVerification, #[error("this account is a child, ask an adult to add this account to a Family")] ChildAccount, #[error("unknown xbox live error: {0}")] UnknownError(u64), } pub struct MicrosoftAuthenticationMethod; impl AuthenticationMethod for MicrosoftAuthenticationMethod { async fn auth(stage: F) -> PolyResult where F: Fn(String, u8, bool) -> (), { stage("Authenticating with Microsoft".into(), 0, false); let msa_code: String = msa_code().await?; stage("Authenticating with Microsoft".into(), 1, false); let msa_token: String = msa_code_to_token(msa_code).await?; stage("Authenticating with Xbox Live".into(), 2, false); let (xbl_token, user_hash): (String, String) = auth_xbl(msa_token).await?; stage("Retrieving XSTS token".into(), 3, false); let xsts_token: String = auth_xsts(xbl_token).await?; stage("Authenticating with Minecraft".into(), 4, false); let access_token: String = auth_minecraft(xsts_token, user_hash).await?; stage("Retrieving Minecraft profile".into(), 5, true); let account: Account = Self::get_profile(access_token).await?; Ok(account) } } async fn auth_minecraft(xsts_token: String, user_hash: String) -> PolyResult { let response = create_client()? .post("https://api.minecraftservices.com/authentication/login_with_xbox") .json(&json!({ "identityToken": format!("XBL3.0 x={};{}", user_hash, xsts_token) })) .send() .await?; let response = response.json::().await?; let access_token = match response.get("access_token") { Some(token) => token.as_str().unwrap(), None => { return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( MicrosoftAuthError::MSAccessToken, ))) } }; Ok(access_token.to_string()) } async fn auth_xsts(token: String) -> PolyResult { let response = create_client()? .post("https://xsts.auth.xboxlive.com/xsts/authorize") .json(&json!({ "Properties": { "SandboxId": "RETAIL", "UserTokens": [ token ] }, "RelyingParty": "rp://api.minecraftservices.com/", "TokenType": "JWT" })) .send() .await?; if response.status().as_u16() == 401 { let error_code = response.json::().await?["XErr"] .as_u64() .unwrap(); match error_code { 2148916233 => { return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( MicrosoftAuthError::NoXBoxAccount, ))) } 2148916235 => { return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( MicrosoftAuthError::XBoxLiveBlocked, ))) } 2148916236 | 2148916237 => { return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( MicrosoftAuthError::AdultVerification, ))) } 2148916238 => { return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( MicrosoftAuthError::ChildAccount, ))) } _ => { return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( MicrosoftAuthError::UnknownError(error_code), ))) } } } let response = response.json::().await?; let xsts_token = match response.get("Token") { Some(token) => token.as_str().unwrap(), None => { return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( MicrosoftAuthError::XSTSToken, ))) } }; Ok(xsts_token.to_string()) } async fn auth_xbl(code: String) -> PolyResult<(String, String)> { let response = create_client()? .post("https://user.auth.xboxlive.com/user/authenticate") .json(&json!({ "Properties": { "AuthMethod": "RPS", "SiteName": "user.auth.xboxlive.com", "RpsTicket": &format!("d={}", code) }, "RelyingParty": "http://auth.xboxlive.com", "TokenType": "JWT" })) .send() .await?; let response = response.json::().await?; let xbl_token = match response.get("Token") { Some(token) => token.as_str().unwrap(), None => { return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( MicrosoftAuthError::XBLToken, ))) } }; let user_hash = match response.get("DisplayClaims").and_then(|claims| { claims .get("xui") .and_then(|xui| xui.get(0).and_then(|xui| xui.get("uhs"))) }) { Some(hash) => hash.as_str().unwrap(), None => { return Err(PolyError::AuthError(AuthenticationError::MicrosoftError( MicrosoftAuthError::XBLUserHash, ))) } }; Ok((xbl_token.to_string(), user_hash.to_string())) } async fn msa_code_to_token(code: String) -> PolyResult { let token = create_client()? .post("https://login.live.com/oauth20_token.srf") .form(&[ ("client_id", CLIENT_ID), ("redirect_uri", &format!("http://localhost:{}/", constants::MSA_PORT)), ("code", &code), ("grant_type", "authorization_code"), ]) .send() .await? .json::() .await?; Ok(token["access_token"].as_str().unwrap().to_string()) } async fn msa_code() -> PolyResult { let url = format!( "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id={}&response_type=code&redirect_uri=http://localhost:{}/&response_mode=query&scope=XboxLive.signin%20offline_access&prompt=consent", CLIENT_ID, constants::MSA_PORT ); open::that(url)?; let mut token: String = String::new(); let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], constants::MSA_PORT)))?; println!("Started local server for MSA on port {}", constants::MSA_PORT); for conn in listener.incoming() { match conn { Err(err) => return Err(err.into()), Ok(stream) => { if let Some(code) = handle_connection(stream) { token = code; break; } } } } Ok(token) } fn handle_connection(mut stream: TcpStream) -> Option { let mut buffer = [0; 512]; if let Err(err) = stream.read(&mut buffer) { eprintln!("Failed to read from stream: {}", err); return None; } let request = String::from_utf8_lossy(&buffer[..]); let url = request.split_whitespace().nth(1)?; let code = url .split('?') .nth(1)? .split('&') .find(|s| s.starts_with("code="))? .split_once('=') .unwrap() .1; let response = r" Microsoft Authentication

Microsoft Authentication

Authentication successful. You can close this window now.

"; stream .write_all( format!( "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}", response.len(), response ) .as_bytes(), ) .unwrap(); Some(code.to_string()) }