use std::collections::HashMap;
use std::fmt::Debug;
use getset::{CopyGetters, Getters};
use nutype::nutype;
use reqwest::{Client, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::json;
use thiserror::Error;
const MINECRAFT_LOGIN_WITH_XBOX: &str = "https://api.minecraftservices.com/authentication/login_with_xbox";
const XBOX_USER_AUTHENTICATE: &str = "https://user.auth.xboxlive.com/user/authenticate";
const XBOX_XSTS_AUTHORIZE: &str = "https://xsts.auth.xboxlive.com/xsts/authorize";
#[nutype(
validate(not_empty),
derive(Clone, PartialEq, Eq, Hash, Deserialize, Serialize, AsRef, Into)
)]
pub struct MinecraftAccessToken(String);
impl Debug for MinecraftAccessToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("MinecraftAccessToken").field(&"[redacted]").finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
pub enum MinecraftTokenType {
Bearer,
}
#[derive(Error, Debug)]
pub enum MinecraftAuthorizationError {
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
#[error("Minor must be added to microsoft family")]
AddToFamily,
#[error("Account does not have xbox")]
NoXbox,
#[error("missing claims from response")]
MissingClaims,
}
#[derive(Deserialize, Serialize, Debug, Getters, CopyGetters, Clone)]
pub struct MinecraftAuthenticationResponse {
#[getset(get = "pub")]
username: String,
#[getset(get = "pub")]
access_token: MinecraftAccessToken,
#[getset(get = "pub")]
token_type: MinecraftTokenType,
#[getset(get_copy = "pub")]
expires_in: u32,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
struct XboxLiveAuthenticationResponse {
token: String,
display_claims: HashMap<String, Vec<HashMap<String, String>>>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct XboxLiveAuthenticationResponseError {
identity: String,
x_err: i64,
message: String,
redirect: String,
}
pub struct MinecraftAuthorizationFlow {
http_client: Client,
}
impl MinecraftAuthorizationFlow {
pub const fn new(http_client: Client) -> Self {
Self { http_client }
}
pub async fn exchange_microsoft_token(
&self, microsoft_access_token: impl AsRef<str>,
) -> Result<MinecraftAuthenticationResponse, MinecraftAuthorizationError> {
let (xbox_token, user_hash) = self.xbox_token(microsoft_access_token).await?;
let xbox_security_token = self.xbox_security_token(xbox_token).await?;
let response = self
.http_client
.post(MINECRAFT_LOGIN_WITH_XBOX)
.json(&json!({
"identityToken":
format!(
"XBL3.0 x={user_hash};{xsts_token}",
user_hash = user_hash,
xsts_token = xbox_security_token.token
)
}))
.send()
.await?;
response.error_for_status_ref()?;
let response = response.json().await?;
Ok(response)
}
async fn xbox_security_token(
&self, xbox_token: String,
) -> Result<XboxLiveAuthenticationResponse, MinecraftAuthorizationError> {
let response = self
.http_client
.post(XBOX_XSTS_AUTHORIZE)
.json(&json!({
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [xbox_token]
},
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT"
}))
.send()
.await?;
if response.status() == StatusCode::UNAUTHORIZED {
let xbox_security_token_err_resp_res = response.json().await;
if xbox_security_token_err_resp_res.is_err() {
return Err(MinecraftAuthorizationError::MissingClaims);
}
let xbox_security_token_err_resp: XboxLiveAuthenticationResponseError =
xbox_security_token_err_resp_res.expect("This should succeed always");
match xbox_security_token_err_resp.x_err {
2148916238 => Err(MinecraftAuthorizationError::AddToFamily),
2148916233 => Err(MinecraftAuthorizationError::NoXbox),
_ => Err(MinecraftAuthorizationError::MissingClaims),
}
} else {
response.error_for_status_ref()?;
let xbox_security_token_resp: XboxLiveAuthenticationResponse = response.json().await?;
Ok(xbox_security_token_resp)
}
}
async fn xbox_token(
&self, microsoft_access_token: impl AsRef<str>,
) -> Result<(String, String), MinecraftAuthorizationError> {
let xbox_authenticate_json = json!({
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": &format!("d={}", microsoft_access_token.as_ref())
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
});
let response = self
.http_client
.post(XBOX_USER_AUTHENTICATE)
.json(&xbox_authenticate_json)
.send()
.await?;
response.error_for_status_ref()?;
let xbox_resp: XboxLiveAuthenticationResponse = response.json().await?;
let xbox_token = xbox_resp.token;
let user_hash = xbox_resp
.display_claims
.get("xui")
.ok_or(MinecraftAuthorizationError::MissingClaims)?
.first()
.ok_or(MinecraftAuthorizationError::MissingClaims)?
.get("uhs")
.ok_or(MinecraftAuthorizationError::MissingClaims)?
.to_owned();
Ok((xbox_token, user_hash))
}
}