use super::*;
use crate::error::{Result, LateJavaCoreError};
use reqwest::Client;
use serde_json::json;
use uuid::Uuid;
use base64::{Engine as _, engine::general_purpose};
pub struct MicrosoftAuth {
client_id: String,
client: Client,
}
impl MicrosoftAuth {
pub fn new(client_id: String) -> Self {
Self {
client_id,
client: Client::new(),
}
}
pub async fn authenticate(&self) -> Result<AuthResponse> {
let device_code = self.get_device_code().await?;
println!("Por favor, ve a: {}", device_code.verification_uri);
println!("E ingresa el código: {}", device_code.user_code);
println!("Presiona Enter cuando hayas completado la autenticación...");
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let token_response = self.exchange_device_code(&device_code).await?;
self.get_account(&token_response).await
}
pub async fn refresh(&self, auth: AuthResponse) -> Result<AuthResponse> {
let refresh_token = auth.refresh_token
.ok_or_else(|| LateJavaCoreError::Auth("No refresh token available".to_string()))?;
let response = self.client
.post("https://login.live.com/oauth20_token.srf")
.form(&[
("grant_type", "refresh_token"),
("client_id", &self.client_id),
("refresh_token", &refresh_token),
])
.send()
.await?;
let token_response: serde_json::Value = response.json().await?;
if let Some(error) = token_response.get("error") {
return Err(LateJavaCoreError::Auth(format!("OAuth error: {}", error)));
}
self.get_account(&token_response).await
}
async fn get_device_code(&self) -> Result<DeviceCodeResponse> {
let response = self.client
.post("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode")
.form(&[
("client_id", &self.client_id),
("scope", &"XboxLive.signin offline_access".to_string()),
])
.send()
.await?;
let device_code: DeviceCodeResponse = response.json().await?;
Ok(device_code)
}
async fn exchange_device_code(&self, device_code: &DeviceCodeResponse) -> Result<serde_json::Value> {
let response = self.client
.post("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")
.form(&[
("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
("client_id", &self.client_id),
("device_code", &device_code.device_code),
])
.send()
.await?;
let token_response: serde_json::Value = response.json().await?;
if let Some(error) = token_response.get("error") {
return Err(LateJavaCoreError::Auth(format!("OAuth error: {}", error)));
}
Ok(token_response)
}
async fn get_account(&self, oauth2: &serde_json::Value) -> Result<AuthResponse> {
let access_token = oauth2["access_token"]
.as_str()
.ok_or_else(|| LateJavaCoreError::Auth("No access token in response".to_string()))?;
let xbl_response = self.authenticate_xbox_live(access_token).await?;
let xbl_token = xbl_response["Token"]
.as_str()
.ok_or_else(|| LateJavaCoreError::Auth("No XBL token".to_string()))?;
let xsts_response = self.authorize_xsts(xbl_token).await?;
let xsts_token = xsts_response["Token"]
.as_str()
.ok_or_else(|| LateJavaCoreError::Auth("No XSTS token".to_string()))?;
let mc_login = self.login_minecraft(&xbl_response, xsts_token).await?;
let mc_access_token = mc_login["access_token"]
.as_str()
.ok_or_else(|| LateJavaCoreError::Auth("No Minecraft access token".to_string()))?;
self.verify_minecraft_entitlements(mc_access_token).await?;
let profile = self.get_minecraft_profile(mc_access_token).await?;
let xbox_account = self.get_xbox_account_info(&xbl_response).await?;
Ok(AuthResponse {
access_token: mc_access_token.to_string(),
client_token: Uuid::new_v4().to_string(),
uuid: profile["id"].as_str().unwrap_or("").to_string(),
name: profile["name"].as_str().unwrap_or("").to_string(),
refresh_token: oauth2["refresh_token"].as_str().map(|s| s.to_string()),
user_properties: "{}".to_string(),
meta: AuthMeta {
auth_type: "Xbox".to_string(),
access_token_expires_in: chrono::Utc::now().timestamp() as u64 +
oauth2["expires_in"].as_u64().unwrap_or(3600),
demo: false,
},
xbox_account: Some(xbox_account),
profile: Some(MinecraftProfile {
skins: profile["skins"].as_array()
.map(|arr| arr.iter().filter_map(|s| serde_json::from_value(s.clone()).ok()).collect())
.unwrap_or_default(),
capes: profile["capes"].as_array()
.map(|arr| arr.iter().filter_map(|s| serde_json::from_value(s.clone()).ok()).collect())
.unwrap_or_default(),
}),
})
}
async fn authenticate_xbox_live(&self, access_token: &str) -> Result<serde_json::Value> {
let response = self.client
.post("https://user.auth.xboxlive.com/user/authenticate")
.json(&json!({
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": format!("d={}", access_token)
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
}))
.send()
.await?;
let xbl: serde_json::Value = response.json().await?;
if let Some(error) = xbl.get("error") {
return Err(LateJavaCoreError::Auth(format!("XBL error: {}", error)));
}
Ok(xbl)
}
async fn authorize_xsts(&self, xbl_token: &str) -> Result<serde_json::Value> {
let response = self.client
.post("https://xsts.auth.xboxlive.com/xsts/authorize")
.json(&json!({
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [xbl_token]
},
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT"
}))
.send()
.await?;
let xsts: serde_json::Value = response.json().await?;
if let Some(error) = xsts.get("error") {
return Err(LateJavaCoreError::Auth(format!("XSTS error: {}", error)));
}
Ok(xsts)
}
async fn login_minecraft(&self, xbl: &serde_json::Value, xsts_token: &str) -> Result<serde_json::Value> {
let uhs = xbl["DisplayClaims"]["xui"][0]["uhs"]
.as_str()
.ok_or_else(|| LateJavaCoreError::Auth("No UHS in XBL response".to_string()))?;
let response = self.client
.post("https://api.minecraftservices.com/authentication/login_with_xbox")
.json(&json!({
"identityToken": format!("XBL3.0 x={};{}", uhs, xsts_token)
}))
.send()
.await?;
let mc_login: serde_json::Value = response.json().await?;
if let Some(error) = mc_login.get("error") {
return Err(LateJavaCoreError::Auth(format!("Minecraft login error: {}", error)));
}
if mc_login["username"].is_null() {
return Err(LateJavaCoreError::Auth("No Minecraft account found".to_string()));
}
Ok(mc_login)
}
async fn verify_minecraft_entitlements(&self, access_token: &str) -> Result<()> {
let response = self.client
.get("https://api.minecraftservices.com/entitlements/mcstore")
.header("Authorization", format!("Bearer {}", access_token))
.send()
.await?;
let mcstore: serde_json::Value = response.json().await?;
if let Some(error) = mcstore.get("error") {
return Err(LateJavaCoreError::Auth(format!("Minecraft store error: {}", error)));
}
let items = mcstore["items"].as_array()
.ok_or_else(|| LateJavaCoreError::Auth("No items in store response".to_string()))?;
let has_minecraft = items.iter().any(|item| {
item["name"].as_str().map(|name|
name == "game_minecraft" || name == "product_minecraft"
).unwrap_or(false)
});
if !has_minecraft {
return Err(LateJavaCoreError::Auth("No Minecraft entitlements found".to_string()));
}
Ok(())
}
async fn get_minecraft_profile(&self, access_token: &str) -> Result<serde_json::Value> {
let response = self.client
.get("https://api.minecraftservices.com/minecraft/profile")
.header("Authorization", format!("Bearer {}", access_token))
.send()
.await?;
let profile: serde_json::Value = response.json().await?;
if let Some(error) = profile.get("error") {
return Err(LateJavaCoreError::Auth(format!("Profile error: {}", error)));
}
Ok(profile)
}
async fn get_xbox_account_info(&self, xbl: &serde_json::Value) -> Result<XboxAccount> {
let xui = &xbl["DisplayClaims"]["xui"][0];
Ok(XboxAccount {
xuid: xui["xid"].as_str().unwrap_or("").to_string(),
gamertag: xui["gt"].as_str().unwrap_or("").to_string(),
age_group: xui["ag"].as_str().unwrap_or("").to_string(),
})
}
}
#[derive(Debug, serde::Deserialize)]
struct DeviceCodeResponse {
device_code: String,
user_code: String,
verification_uri: String,
expires_in: u64,
interval: u64,
message: String,
}