aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdam Reisenauer <58893124+Mythbusters123@users.noreply.github.com>2023-06-24 18:09:43 -0400
committerGitHub <noreply@github.com>2023-06-24 17:09:43 -0500
commit5e4699688207b8ac722ae7f96c49428242f49a9d (patch)
tree52b0f9b8507fa380376dab81411cb74eed413c95
parentfe687f9bdbdf3e0214ac4ac6da47a181e4dc23dd (diff)
downloadazalea-drasl-5e4699688207b8ac722ae7f96c49428242f49a9d.tar.xz
Add functions `auth_with_link_code`, `get_ms_link_code`, and `get_ms_auth_token`. (#88)
* Add option for grabbing authentication code for Microsoft seperately. Created two new functions, one that outputs the DeviceCodeResponse and one that uses this response to authenticate an actual account. * Added documentation and cleaned up function names. Still wondering about code repeition * reduce code duplication, more docs, cleanup * clippy --------- Co-authored-by: mat <git@matdoes.dev>
-rw-r--r--Cargo.lock1
-rwxr-xr-xazalea-auth/examples/auth_manual.rs32
-rwxr-xr-xazalea-auth/src/auth.rs220
-rwxr-xr-xazalea-auth/src/cache.rs9
-rwxr-xr-xazalea-auth/src/lib.rs2
-rw-r--r--azalea-client/Cargo.toml1
-rwxr-xr-xazalea-client/src/account.rs89
-rw-r--r--azalea-client/src/inventory.rs2
-rw-r--r--azalea-inventory/azalea-inventory-macros/src/menu_impl.rs1
9 files changed, 268 insertions, 89 deletions
diff --git a/Cargo.lock b/Cargo.lock
index ac8e92e8..d29c12b2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -309,6 +309,7 @@ dependencies = [
"once_cell",
"parking_lot",
"regex",
+ "reqwest",
"thiserror",
"tokio",
"uuid",
diff --git a/azalea-auth/examples/auth_manual.rs b/azalea-auth/examples/auth_manual.rs
new file mode 100755
index 00000000..6a9510a8
--- /dev/null
+++ b/azalea-auth/examples/auth_manual.rs
@@ -0,0 +1,32 @@
+//! Authenticate with Microsoft and get a Minecraft profile, but don't cache and
+//! use our own code to display the link code.
+//!
+//! If you still want it to cache, look at the code in [`azalea_auth::auth`] and
+//! see how that does it.
+
+use std::error::Error;
+
+use azalea_auth::ProfileResponse;
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn Error>> {
+ env_logger::init();
+
+ let profile = auth().await?;
+ println!("Logged in as {}", profile.name);
+
+ Ok(())
+}
+
+async fn auth() -> Result<ProfileResponse, Box<dyn Error>> {
+ let client = reqwest::Client::new();
+
+ let res = azalea_auth::get_ms_link_code(&client).await?;
+ println!(
+ "Go to {} and enter the code {}",
+ res.verification_uri, res.user_code
+ );
+ let msa = azalea_auth::get_ms_auth_token(&client, res).await?;
+ let auth_result = azalea_auth::get_minecraft_token(&client, &msa.data.access_token).await?;
+ Ok(azalea_auth::get_profile(&client, &auth_result.minecraft_access_token).await?)
+}
diff --git a/azalea-auth/src/auth.rs b/azalea-auth/src/auth.rs
index bb72c8c4..b0b30e1f 100755
--- a/azalea-auth/src/auth.rs
+++ b/azalea-auth/src/auth.rs
@@ -55,6 +55,10 @@ pub enum AuthError {
/// The email is technically only used as a cache key, so it *could* be
/// anything. You should just have it be the actual email so it's not confusing
/// though, and in case the Microsoft API does start providing the real email.
+///
+/// If you want to use your own code to cache or show the auth code to the user
+/// in a different way, use [`get_ms_link_code`], [`get_ms_auth_token`],
+/// [`get_minecraft_token`] and [`get_profile`] instead instead.
pub async fn auth(email: &str, opts: AuthOpts) -> Result<AuthResult, AuthError> {
let cached_account = if let Some(cache_file) = &opts.cache_file {
cache::get_account_in_cache(cache_file, email).await
@@ -62,16 +66,15 @@ pub async fn auth(email: &str, opts: AuthOpts) -> Result<AuthResult, AuthError>
None
};
- // these two MUST be set by the end, since we return them in AuthResult
- let profile: ProfileResponse;
- let minecraft_access_token: String;
-
if cached_account.is_some() && !cached_account.as_ref().unwrap().mca.is_expired() {
let account = cached_account.as_ref().unwrap();
// the minecraft auth data is cached and not expired, so we can just
// use that instead of doing auth all over again :)
- profile = account.profile.clone();
- minecraft_access_token = account.mca.data.access_token.clone();
+
+ Ok(AuthResult {
+ access_token: account.mca.data.access_token.clone(),
+ profile: account.profile.clone(),
+ })
} else {
let client = reqwest::Client::new();
let mut msa = if let Some(account) = cached_account {
@@ -83,37 +86,20 @@ pub async fn auth(email: &str, opts: AuthOpts) -> Result<AuthResult, AuthError>
log::trace!("refreshing Microsoft auth token");
msa = refresh_ms_auth_token(&client, &msa.data.refresh_token).await?;
}
- let ms_access_token = &msa.data.access_token;
- log::trace!("Got access token: {}", ms_access_token);
-
- let xbl_auth = auth_with_xbox_live(&client, ms_access_token).await?;
-
- let xsts_token = obtain_xsts_for_minecraft(
- &client,
- &xbl_auth
- .get()
- .expect("Xbox Live auth token shouldn't have expired yet")
- .token,
- )
- .await?;
- // Minecraft auth
- let mca = auth_with_minecraft(&client, &xbl_auth.data.user_hash, &xsts_token).await?;
+ let msa_token = &msa.data.access_token;
+ log::trace!("Got access token: {msa_token}");
- minecraft_access_token = mca
- .get()
- .expect("Minecraft auth shouldn't have expired yet")
- .access_token
- .to_string();
+ let res = get_minecraft_token(&client, msa_token).await?;
if opts.check_ownership {
- let has_game = check_ownership(&client, &minecraft_access_token).await?;
+ let has_game = check_ownership(&client, &res.minecraft_access_token).await?;
if !has_game {
return Err(AuthError::DoesNotOwnGame);
}
}
- profile = get_profile(&client, &minecraft_access_token).await?;
+ let profile: ProfileResponse = get_profile(&client, &res.minecraft_access_token).await?;
if let Some(cache_file) = opts.cache_file {
if let Err(e) = cache::set_account_in_cache(
@@ -121,9 +107,9 @@ pub async fn auth(email: &str, opts: AuthOpts) -> Result<AuthResult, AuthError>
email,
CachedAccount {
email: email.to_string(),
- mca,
+ mca: res.mca,
msa,
- xbl: xbl_auth,
+ xbl: res.xbl,
profile: profile.clone(),
},
)
@@ -132,15 +118,60 @@ pub async fn auth(email: &str, opts: AuthOpts) -> Result<AuthResult, AuthError>
log::error!("{}", e);
}
}
+
+ Ok(AuthResult {
+ access_token: res.minecraft_access_token,
+ profile,
+ })
}
+}
+
+/// Authenticate with Minecraft when we already have a Microsoft auth token.
+///
+/// Usually you don't need this since [`auth`] will call it for you, but it's
+/// useful if you want more control over what it does.
+///
+/// If you don't have a Microsoft auth token, you can get it from
+/// [`get_ms_link_code`] and then [`get_ms_auth_token`].
+pub async fn get_minecraft_token(
+ client: &reqwest::Client,
+ msa: &str,
+) -> Result<MinecraftTokenResponse, AuthError> {
+ let xbl_auth = auth_with_xbox_live(client, msa).await?;
- Ok(AuthResult {
- access_token: minecraft_access_token,
- profile,
+ let xsts_token = obtain_xsts_for_minecraft(
+ client,
+ &xbl_auth
+ .get()
+ .expect("Xbox Live auth token shouldn't have expired yet")
+ .token,
+ )
+ .await?;
+
+ // Minecraft auth
+ let mca = auth_with_minecraft(client, &xbl_auth.data.user_hash, &xsts_token).await?;
+
+ let minecraft_access_token: String = mca
+ .get()
+ .expect("Minecraft auth shouldn't have expired yet")
+ .access_token
+ .to_string();
+
+ Ok(MinecraftTokenResponse {
+ mca,
+ xbl: xbl_auth,
+ minecraft_access_token,
})
}
#[derive(Debug)]
+pub struct MinecraftTokenResponse {
+ pub mca: ExpiringValue<MinecraftAuthResponse>,
+ pub xbl: ExpiringValue<XboxLiveAuth>,
+ pub minecraft_access_token: String,
+}
+
+#[derive(Debug)]
pub struct AuthResult {
pub access_token: String,
pub profile: ProfileResponse,
@@ -148,64 +179,63 @@ pub struct AuthResult {
#[derive(Debug, Deserialize)]
pub struct DeviceCodeResponse {
- user_code: String,
- device_code: String,
- verification_uri: String,
- expires_in: u64,
- interval: u64,
+ pub user_code: String,
+ pub device_code: String,
+ pub verification_uri: String,
+ pub expires_in: u64,
+ pub interval: u64,
}
#[allow(unused)]
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct AccessTokenResponse {
- token_type: String,
- expires_in: u64,
- scope: String,
- access_token: String,
- refresh_token: String,
- user_id: String,
+ pub token_type: String,
+ pub expires_in: u64,
+ pub scope: String,
+ pub access_token: String,
+ pub refresh_token: String,
+ pub user_id: String,
}
#[allow(unused)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct XboxLiveAuthResponse {
- issue_instant: String,
- not_after: String,
- token: String,
- display_claims: HashMap<String, Vec<HashMap<String, String>>>,
+ pub issue_instant: String,
+ pub not_after: String,
+ pub token: String,
+ pub display_claims: HashMap<String, Vec<HashMap<String, String>>>,
}
/// Just the important data
#[derive(Serialize, Deserialize, Debug)]
pub struct XboxLiveAuth {
- token: String,
- user_hash: String,
+ pub token: String,
+ pub user_hash: String,
}
#[allow(unused)]
#[derive(Debug, Deserialize, Serialize)]
pub struct MinecraftAuthResponse {
- username: String,
- roles: Vec<String>,
- access_token: String,
- token_type: String,
- expires_in: u64,
+ pub username: String,
+ pub roles: Vec<String>,
+ pub access_token: String,
+ pub token_type: String,
+ pub expires_in: u64,
}
-#[allow(unused)]
#[derive(Debug, Deserialize)]
pub struct GameOwnershipResponse {
- items: Vec<GameOwnershipItem>,
- signature: String,
- key_id: String,
+ pub items: Vec<GameOwnershipItem>,
+ pub signature: String,
+ pub key_id: String,
}
#[allow(unused)]
#[derive(Debug, Deserialize)]
pub struct GameOwnershipItem {
- name: String,
- signature: String,
+ pub name: String,
+ pub signature: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
@@ -227,12 +257,33 @@ pub enum GetMicrosoftAuthTokenError {
Timeout,
}
-/// Asks the user to go to a webpage and log in with Microsoft.
-async fn interactive_get_ms_auth_token(
+/// Get the Microsoft link code that's shown to the user for logging into
+/// Microsoft.
+///
+/// You should call [`get_ms_auth_token`] right after showing the user the
+/// [`verification_uri`](DeviceCodeResponse::verification_uri) and
+/// [`user_code`](DeviceCodeResponse::user_code).
+///
+/// If showing the link code in the terminal is acceptable, then you can just
+/// use [`interactive_get_ms_auth_token`] instead.
+///
+/// ```
+/// # async fn example(client: &reqwest::Client) -> Result<(), Box<dyn std::error::Error>> {
+/// let res = azalea_auth::get_ms_link_code(&client).await?;
+/// println!(
+/// "Go to {} and enter the code {}",
+/// res.verification_uri, res.user_code
+/// );
+/// let msa = azalea_auth::get_ms_auth_token(client, res).await?;
+/// let minecraft = azalea_auth::get_minecraft_token(client, &msa.data.access_token).await?;
+/// let profile = azalea_auth::get_profile(&client, &minecraft.minecraft_access_token).await?;
+/// # Ok(())
+/// # }
+/// ```
+pub async fn get_ms_link_code(
client: &reqwest::Client,
- email: &str,
-) -> Result<ExpiringValue<AccessTokenResponse>, GetMicrosoftAuthTokenError> {
- let res = client
+) -> Result<DeviceCodeResponse, GetMicrosoftAuthTokenError> {
+ Ok(client
.post("https://login.live.com/oauth20_connect.srf")
.form(&vec![
("scope", "service::user.auth.xboxlive.com::MBI_SSL"),
@@ -242,13 +293,17 @@ async fn interactive_get_ms_auth_token(
.send()
.await?
.json::<DeviceCodeResponse>()
- .await?;
- log::trace!("Device code response: {:?}", res);
- println!(
- "Go to \x1b[1m{}\x1b[m and enter the code \x1b[1m{}\x1b[m for \x1b[1m{}\x1b[m",
- res.verification_uri, res.user_code, email
- );
+ .await?)
+}
+/// Wait until the user logged into Microsoft with the given code. You get the
+/// device code response needed for this function from [`get_ms_link_code`].
+///
+/// You should pass the response from this to [`get_minecraft_token`].
+pub async fn get_ms_auth_token(
+ client: &reqwest::Client,
+ res: DeviceCodeResponse,
+) -> Result<ExpiringValue<AccessTokenResponse>, GetMicrosoftAuthTokenError> {
let login_expires_at = Instant::now() + std::time::Duration::from_secs(res.expires_in);
while Instant::now() < login_expires_at {
@@ -285,13 +340,30 @@ async fn interactive_get_ms_auth_token(
Err(GetMicrosoftAuthTokenError::Timeout)
}
+/// Asks the user to go to a webpage and log in with Microsoft. If you need to
+/// access the code, then use [`get_ms_link_code`] and then
+/// [`get_ms_auth_token`] instead.
+pub async fn interactive_get_ms_auth_token(
+ client: &reqwest::Client,
+ email: &str,
+) -> Result<ExpiringValue<AccessTokenResponse>, GetMicrosoftAuthTokenError> {
+ let res = get_ms_link_code(client).await?;
+ log::trace!("Device code response: {:?}", res);
+ println!(
+ "Go to \x1b[1m{}\x1b[m and enter the code \x1b[1m{}\x1b[m for \x1b[1m{}\x1b[m",
+ res.verification_uri, res.user_code, email
+ );
+
+ get_ms_auth_token(client, res).await
+}
+
#[derive(Debug, Error)]
pub enum RefreshMicrosoftAuthTokenError {
#[error("Http error: {0}")]
Http(#[from] reqwest::Error),
}
-async fn refresh_ms_auth_token(
+pub async fn refresh_ms_auth_token(
client: &reqwest::Client,
refresh_token: &str,
) -> Result<ExpiringValue<AccessTokenResponse>, RefreshMicrosoftAuthTokenError> {
diff --git a/azalea-auth/src/cache.rs b/azalea-auth/src/cache.rs
index 404b05ff..1e8aee10 100755
--- a/azalea-auth/src/cache.rs
+++ b/azalea-auth/src/cache.rs
@@ -58,6 +58,15 @@ impl<T> ExpiringValue<T> {
}
}
+impl<T: Clone> Clone for ExpiringValue<T> {
+ fn clone(&self) -> Self {
+ Self {
+ expires_at: self.expires_at,
+ data: self.data.clone(),
+ }
+ }
+}
+
async fn get_entire_cache(cache_file: &Path) -> Result<Vec<CachedAccount>, CacheError> {
let mut cache: Vec<CachedAccount> = Vec::new();
if cache_file.exists() {
diff --git a/azalea-auth/src/lib.rs b/azalea-auth/src/lib.rs
index cf0d0401..bd151eb3 100755
--- a/azalea-auth/src/lib.rs
+++ b/azalea-auth/src/lib.rs
@@ -1,7 +1,7 @@
#![doc = include_str!("../README.md")]
mod auth;
-mod cache;
+pub mod cache;
pub mod certs;
pub mod game_profile;
pub mod sessionserver;
diff --git a/azalea-client/Cargo.toml b/azalea-client/Cargo.toml
index 09a682b0..0303000f 100644
--- a/azalea-client/Cargo.toml
+++ b/azalea-client/Cargo.toml
@@ -9,6 +9,7 @@ version = "0.7.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+reqwest = { version = "0.11.12", default-features = false }
anyhow = "1.0.59"
async-trait = "0.1.58"
azalea-auth = { path = "../azalea-auth", version = "0.7.0" }
diff --git a/azalea-client/src/account.rs b/azalea-client/src/account.rs
index 5c6056c1..2d1b766c 100755
--- a/azalea-client/src/account.rs
+++ b/azalea-client/src/account.rs
@@ -4,6 +4,7 @@ use std::sync::Arc;
use crate::get_mc_dir;
use azalea_auth::certs::{Certificates, FetchCertificatesError};
+use azalea_auth::AccessTokenResponse;
use parking_lot::Mutex;
use thiserror::Error;
use uuid::Uuid;
@@ -55,8 +56,15 @@ pub struct Account {
/// The parameters that were passed for creating the associated [`Account`].
#[derive(Clone, Debug)]
pub enum AccountOpts {
- Offline { username: String },
- Microsoft { email: String },
+ Offline {
+ username: String,
+ },
+ Microsoft {
+ email: String,
+ },
+ MicrosoftWithAccessToken {
+ msa: Arc<Mutex<azalea_auth::cache::ExpiringValue<AccessTokenResponse>>>,
+ },
}
impl Account {
@@ -106,27 +114,82 @@ impl Account {
})
}
+ /// This will create an online-mode account through
+ /// [`azalea_auth::get_minecraft_token`] so you can have more control over
+ /// the authentication process (like doing your own caching or
+ /// displaying the Microsoft user code to the user in a different way).
+ ///
+ /// Note that this will not refresh the token when it expires.
+ ///
+ /// ```
+ /// # use azalea_client::Account;
+ /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
+ /// let client = reqwest::Client::new();
+ ///
+ /// let res = azalea_auth::get_ms_link_code(&client).await?;
+ /// println!(
+ /// "Go to {} and enter the code {}",
+ /// res.verification_uri, res.user_code
+ /// );
+ /// let msa = azalea_auth::get_ms_auth_token(&client, res).await?;
+ /// Account::with_microsoft_access_token(msa).await?;
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub async fn with_microsoft_access_token(
+ mut msa: azalea_auth::cache::ExpiringValue<AccessTokenResponse>,
+ ) -> Result<Self, azalea_auth::AuthError> {
+ let client = reqwest::Client::new();
+
+ if msa.is_expired() {
+ log::trace!("refreshing Microsoft auth token");
+ msa = azalea_auth::refresh_ms_auth_token(&client, &msa.data.refresh_token).await?;
+ }
+
+ let msa_token = &msa.data.access_token;
+
+ let res = azalea_auth::get_minecraft_token(&client, msa_token).await?;
+
+ let profile = azalea_auth::get_profile(&client, &res.minecraft_access_token).await?;
+
+ Ok(Self {
+ username: profile.name,
+ access_token: Some(Arc::new(Mutex::new(res.minecraft_access_token))),
+ uuid: Some(profile.id),
+ account_opts: AccountOpts::MicrosoftWithAccessToken {
+ msa: Arc::new(Mutex::new(msa)),
+ },
+ certs: None,
+ })
+ }
/// Refresh the access_token for this account to be valid again.
///
/// This requires the `auth_opts` field to be set correctly (which is done
/// by default if you used the constructor functions). Note that if the
- /// Account is offline-mode, this function won't do anything.
+ /// Account is offline-mode then this function won't do anything.
pub async fn refresh(&self) -> Result<(), azalea_auth::AuthError> {
match &self.account_opts {
// offline mode doesn't need to refresh so just don't do anything lol
AccountOpts::Offline { .. } => Ok(()),
AccountOpts::Microsoft { email } => {
let new_account = Account::microsoft(email).await?;
- let access_token = self
- .access_token
- .as_ref()
- .expect("Access token should always be set for Microsoft accounts");
- let new_access_token = new_account
- .access_token
- .expect("Access token should always be set for Microsoft accounts")
- .lock()
- .clone();
- *access_token.lock() = new_access_token;
+ let access_token_mutex = self.access_token.as_ref().unwrap();
+ let new_access_token = new_account.access_token.unwrap().lock().clone();
+ *access_token_mutex.lock() = new_access_token;
+ Ok(())
+ }
+ AccountOpts::MicrosoftWithAccessToken { msa } => {
+ let msa_value = msa.lock().clone();
+ let new_account = Account::with_microsoft_access_token(msa_value).await?;
+
+ let access_token_mutex = self.access_token.as_ref().unwrap();
+ let new_access_token = new_account.access_token.unwrap().lock().clone();
+
+ *access_token_mutex.lock() = new_access_token;
+ let AccountOpts::MicrosoftWithAccessToken { msa: new_msa } =
+ new_account.account_opts else { unreachable!() };
+ *msa.lock() = new_msa.lock().clone();
+
Ok(())
}
}
diff --git a/azalea-client/src/inventory.rs b/azalea-client/src/inventory.rs
index f8c2b2a4..b7269ef1 100644
--- a/azalea-client/src/inventory.rs
+++ b/azalea-client/src/inventory.rs
@@ -79,7 +79,7 @@ pub struct InventoryComponent {
/// The item that is currently held by the cursor. `Slot::Empty` if nothing
/// is currently being held.
///
- /// This is different from [`Self::hotbar_selected_index`], which is the
+ /// This is different from [`Self::selected_hotbar_slot`], which is the
/// item that's selected in the hotbar.
pub carried: ItemSlot,
/// An identifier used by the server to track client inventory desyncs. This
diff --git a/azalea-inventory/azalea-inventory-macros/src/menu_impl.rs b/azalea-inventory/azalea-inventory-macros/src/menu_impl.rs
index 194577ad..a4f78370 100644
--- a/azalea-inventory/azalea-inventory-macros/src/menu_impl.rs
+++ b/azalea-inventory/azalea-inventory-macros/src/menu_impl.rs
@@ -179,6 +179,7 @@ pub fn generate(input: &DeclareMenus) -> TokenStream {
/// Get the range of slot indexes that contain the player's hotbar. This may be different for each menu.
///
/// ```
+ /// # let inventory = azalea_inventory::Menu::Player(azalea_inventory::Player::default());
/// let hotbar_items = &inventory.slots()[inventory.hotbar_slots_range()];
/// ```
pub fn hotbar_slots_range(&self) -> RangeInclusive<usize> {