diff --git a/Cargo.toml b/Cargo.toml index 74cff4d..5aeb01c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "minecraft-msa-auth" -version = "0.3.0" +version = "0.4.0" edition = "2021" authors = ["Celeste Peláez "] -description = "A library for authenticating with Microsoft accounts to access Minecraft services." +description = "A library for authenticating with Microsoft accounts to access online Minecraft services." repository = "https://github.com/KernelFreeze/minecraft-msa-auth" categories = ["authentication"] rust-version = "1.67.1" @@ -12,12 +12,18 @@ license = "MIT OR Apache-2.0" [dependencies] getset = "0.1.2" nutype = { version = "0.3.1", features = ["serde"] } -reqwest = { version = "0.11.14", default-features = false, features = ["json"] } -serde = { version = "1.0.152", features = ["derive"] } -serde_json = "1.0.93" -thiserror = "1.0.38" +reqwest = { version = "0.11.22", default-features = false, features = ["json"] } +serde = { version = "1.0.189", features = ["derive"] } +serde_json = "1.0.107" +thiserror = "1.0.50" [dev-dependencies] -oauth2 = { version = "4.3.0", default-features = false, features = ["reqwest", "rustls-tls"] } -reqwest = { version = "0.11.14", default-features = false, features = ["rustls-tls", "json"] } -tokio = { version = "1.25.0", features = ["full"] } +oauth2 = { version = "4.4.2", default-features = false, features = [ + "reqwest", + "rustls-tls", +] } +reqwest = { version = "0.11.22", default-features = false, features = [ + "rustls-tls", + "json", +] } +tokio = { version = "1.33.0", features = ["full"] } diff --git a/src/lib.rs b/src/lib.rs index 4fded5e..696658f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,13 +56,13 @@ use std::fmt::Debug; use getset::{CopyGetters, Getters}; use nutype::nutype; -use reqwest::{Client as HttpClient, Response, StatusCode}; +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_AUTHERNITATE: &str = "https://user.auth.xboxlive.com/user/authenticate"; +const XBOX_USER_AUTHENTICATE: &str = "https://user.auth.xboxlive.com/user/authenticate"; const XBOX_XSTS_AUTHORIZE: &str = "https://xsts.auth.xboxlive.com/xsts/authorize"; /// Represents a Minecraft access token @@ -86,18 +86,21 @@ pub enum MinecraftTokenType { /// Represents an error that can occur when authenticating with Minecraft. #[derive(Error, Debug)] pub enum MinecraftAuthorizationError { - #[error("Http error: {0}")] - Http(StatusCode, String), - + /// An error occurred while sending the request #[error(transparent)] Reqwest(#[from] reqwest::Error), + + /// Claims were missing from the response + #[error("missing claims from response")] + MissingClaims, } /// The response from Minecraft when attempting to authenticate with an xbox /// token #[derive(Deserialize, Serialize, Debug, Getters, CopyGetters, Clone)] pub struct MinecraftAuthenticationResponse { - /// Some UUID of the account. Not the player's UUID + /// UUID of the Xbox account. + /// Please note that this is not the Minecraft player's UUID #[getset(get = "pub")] username: String, @@ -129,12 +132,13 @@ struct XboxLiveAuthenticationResponse { /// The flow for authenticating with a Microsoft access token and getting a /// Minecraft access token. pub struct MinecraftAuthorizationFlow { - http_client: HttpClient, + http_client: Client, } impl MinecraftAuthorizationFlow { - /// Creates a new [MinecraftAuthorizationFlow]. - pub const fn new(http_client: HttpClient) -> Self { + /// Creates a new [MinecraftAuthorizationFlow] using the given + /// [Client]. + pub const fn new(http_client: Client) -> Self { Self { http_client } } @@ -144,28 +148,31 @@ impl MinecraftAuthorizationFlow { pub async fn exchange_microsoft_token( &self, microsoft_access_token: impl AsRef, ) -> Result { - 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 (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(XBOX_USER_AUTHERNITATE) - .json(&xbox_authenticate_json) + .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?; - let response = error_for_status(response).await?; - let xbox_resp: XboxLiveAuthenticationResponse = response.json().await?; + response.error_for_status_ref()?; - let xbox_token = &xbox_resp.token; - let user_hash = &xbox_resp.display_claims["xui"][0]["uhs"]; + let response = response.json().await?; + Ok(response) + } + async fn xbox_security_token( + &self, xbox_token: String, + ) -> Result { let response = self .http_client .post(XBOX_XSTS_AUTHORIZE) @@ -179,34 +186,41 @@ impl MinecraftAuthorizationFlow { })) .send() .await?; - let response = error_for_status(response).await?; + response.error_for_status_ref()?; let xbox_security_token_resp: XboxLiveAuthenticationResponse = response.json().await?; + Ok(xbox_security_token_resp) + } - let xbox_security_token = &xbox_security_token_resp.token; + async fn xbox_token( + &self, microsoft_access_token: impl AsRef, + ) -> 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(MINECRAFT_LOGIN_WITH_XBOX) - .json(&json!({ - "identityToken": - format!( - "XBL3.0 x={user_hash};{xsts_token}", - user_hash = user_hash, - xsts_token = xbox_security_token - ) - })) + .post(XBOX_USER_AUTHENTICATE) + .json(&xbox_authenticate_json) .send() .await?; - let response = error_for_status(response).await?; - let minecraft_resp: MinecraftAuthenticationResponse = response.json().await?; - Ok(minecraft_resp) - } -} - -async fn error_for_status(response: Response) -> Result { - let status = response.status(); - if !status.is_success() { - let response = response.text().await?; - return Err(MinecraftAuthorizationError::Http(status, response)); + 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)? + .get(0) + .ok_or(MinecraftAuthorizationError::MissingClaims)? + .get("uhs") + .ok_or(MinecraftAuthorizationError::MissingClaims)? + .to_owned(); + Ok((xbox_token, user_hash)) } - Ok(response) }