use std::collections::HashMap;
use std::fmt::Debug;
use reqwest::Client;
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";
#[derive(Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
pub struct MinecraftAccessToken(String);
impl MinecraftAccessToken {
pub fn into_inner(self) -> String {
self.0
}
}
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("missing claims from response")]
MissingClaims,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct MinecraftAuthenticationResponse {
pub username: String,
pub access_token: MinecraftAccessToken,
pub token_type: MinecraftTokenType,
pub expires_in: u32,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
struct XboxLiveAuthenticationResponse {
token: String,
display_claims: HashMap<String, Vec<HashMap<String, 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?;
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))
}
}