use std::fmt::Display;
use anyhow::Context;
use serde::Deserialize;
use super::structs::AuthMethod;
use crate::{password::Password, prelude::*};
pub mod leagcy;
use leagcy::*;
pub struct MicrosoftOAuth<T> {
client_id: T,
}
impl<T: Display> MicrosoftOAuth<T> {
pub const fn new(client_id: T) -> Self {
Self { client_id }
}
pub async fn get_devicecode(&self) -> DynResult<DeviceCodeResponse> {
let res = crate::http::post(
"https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode?mkt=zh-CN",
)
.body_string(format!(
"client_id={}&scope=XboxLive.signin%20offline_access",
self.client_id
))
.content_type("application/x-www-form-urlencoded")
.recv_json::<DeviceCodeResponse>()
.await
.map_err(|err| anyhow::anyhow!("请求设备码时发生错误:{}", err))?;
Ok(res)
}
pub async fn verify_device_code(&self, device_code: &str) -> DynResult<TokenResponse> {
let res =
crate::http::post("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")
.body_string(format!(
"grant_type=urn:ietf:params:oauth:grant-type:device_code&client_id={}&device_code={}",
self.client_id, device_code,
))
.content_type("application/x-www-form-urlencoded")
.recv_json::<TokenResponse>()
.await
.map_err(|err| anyhow::anyhow!("请求设备码时发生错误:{}", err))?;
Ok(res)
}
async fn refresh_token(&self, refresh_token: &str) -> DynResult<TokenResponse> {
let res =
crate::http::post("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")
.body_string(format!(
"grant_type=refresh_token&client_id={}&refresh_token={}",
self.client_id, refresh_token,
))
.content_type("application/x-www-form-urlencoded")
.recv_json::<TokenResponse>()
.await
.map_err(|err| anyhow::anyhow!("请求设备码时发生错误:{}", err))?;
Ok(res)
}
async fn auth_xbox_live(&self, access_token: &str) -> DynResult<(String, String)> {
tracing::debug!("正在验证 Xbox Live 账户");
let xbox_auth_body = format!(
"{\
{\
\"Properties\":{\
{\
\"AuthMethod\":\"RPS\",\
\"SiteName\":\"user.auth.xboxlive.com\",\
\"RpsTicket\":\"d={access_token}\"\
}\
},\
\"RelyingParty\":\"http://auth.xboxlive.com\",\
\"TokenType\":\"JWT\"\
}\
}"
);
let xbox_auth_resp: XBoxAuthResponse =
crate::http::post("https://user.auth.xboxlive.com/user/authenticate")
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.body(xbox_auth_body.as_bytes())
.recv_json()
.await
.map_err(|e| anyhow::anyhow!("验证 Xbox Live 账户失败:{}", e))?;
let token = xbox_auth_resp.token.to_owned();
if let Some(uhs) = xbox_auth_resp.display_claims.xui.first() {
let uhs = uhs.uhs.to_owned();
let xsts_body = format!(
"{\
{\
\"Properties\":{\
{\
\"SandboxId\":\"RETAIL\",\
\"UserTokens\":[\"{token}\"]\
}\
},\
\"RelyingParty\":\"rp://api.minecraftservices.com/\",\
\"TokenType\":\"JWT\"\
}\
}"
);
tracing::debug!("正在获取 XSTS");
let xsts_resp: XBoxAuthResponse =
crate::http::post("https://xsts.auth.xboxlive.com/xsts/authorize")
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.body(xsts_body.as_bytes())
.recv_json()
.await
.map_err(|e| anyhow::anyhow!("获取 XSTS 账户失败:{}", e))?;
let xsts_token = xsts_resp.token;
Ok((uhs, xsts_token))
} else {
anyhow::bail!("获取 UserHash 失败")
}
}
pub async fn start_auth(
&self,
access_token: &str,
refresh_token: &str,
) -> DynResult<AuthMethod> {
let (uhs, xsts_token) = self.auth_xbox_live(access_token).await?;
tracing::debug!("正在获取 XUID");
let xuid = leagcy::get_xuid(&uhs, &xsts_token).await?;
tracing::debug!("正在获取 Mojang 访问令牌");
let access_token = leagcy::get_mojang_access_token(&uhs, &xsts_token).await?;
if access_token.is_empty() {
anyhow::bail!("获取令牌失败")
} else {
tracing::debug!("正在检查是否拥有 Minecraft");
let mcstore_resp =
crate::http::get("https://api.minecraftservices.com/entitlements/mcstore")
.header(
"Authorization",
&format!("Bearer {}", &access_token.as_string()),
)
.await
.map_err(|e| anyhow::anyhow!(e))?
.body_string()
.await
.map_err(|e| anyhow::anyhow!(e))?;
let mcstore_resp: MinecraftStoreResponse = serde_json::from_str(&mcstore_resp)?;
if mcstore_resp.items.is_empty() {
anyhow::bail!(
"没有在已购项目中找到 Minecraft!请检查你的账户是否已购买 Minecraft!"
);
}
tracing::debug!("正在获取 Minecraft 账户信息");
let profile_resp: MinecraftXBoxProfileResponse =
crate::http::get("https://api.minecraftservices.com/minecraft/profile")
.header(
"Authorization",
&format!("Bearer {}", &access_token.as_string()),
)
.await
.map_err(|e| anyhow::anyhow!(e))?
.body_json()
.await
.map_err(|e| anyhow::anyhow!(e))?;
if profile_resp.error.is_empty() {
if let Some(skin) = profile_resp.skins.iter().find(|a| a.state == "ACTIVE") {
tracing::debug!("正在解析皮肤: {}", skin.url);
let skin_data = crate::http::get(&skin.url)
.await
.map_err(|e| anyhow::anyhow!(e))?
.body_bytes()
.await
.map_err(|e| anyhow::anyhow!(e))?;
let (head_skin, hat_skin) =
crate::auth::parse_head_skin(skin_data).context("解析皮肤数据失败")?;
tracing::debug!("微软账户验证成功!");
Ok(AuthMethod::Microsoft {
access_token,
refresh_token: refresh_token.to_string().into(),
xuid,
head_skin,
hat_skin,
player_name: profile_resp.name,
uuid: profile_resp.id,
})
} else {
anyhow::bail!("皮肤获取失败!");
}
} else {
anyhow::bail!(
"没有在账户中找到 Minecraft 账户信息!请检查你的账户是否已购买 Minecraft!"
);
}
}
}
pub async fn refresh_auth(&self, method: &mut AuthMethod) -> DynResult {
if let AuthMethod::Microsoft {
access_token,
refresh_token,
..
} = method
{
tracing::debug!("正在刷新令牌");
let new_token = self.refresh_token(refresh_token.as_str()).await?;
*refresh_token = new_token.refresh_token.into();
let (uhs, xsts_token) = self.auth_xbox_live(&new_token.access_token).await?;
tracing::debug!("正在获取 Mojang 访问令牌");
let new_access_token = leagcy::get_mojang_access_token(&uhs, &xsts_token).await?;
anyhow::ensure!(
!new_access_token.is_empty(),
"刷新令牌失败: {}",
new_access_token
);
*access_token = new_access_token;
Ok(())
} else {
anyhow::bail!("不支持的方法");
}
}
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(default)]
pub struct DeviceCodeResponse {
pub device_code: String,
pub user_code: String,
pub verification_uri: String,
pub expires_in: usize,
pub interval: usize,
pub message: String,
pub error: String,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(default)]
pub struct TokenResponse {
pub token_type: String,
pub scope: String,
pub expires_in: usize,
pub access_token: Password,
pub id_token: String,
pub refresh_token: String,
pub error: String,
}