diff options
| author | mat <27899617+mat-1@users.noreply.github.com> | 2026-01-01 22:28:53 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-01 22:28:53 -0600 |
| commit | 1ca1f1d9e27aeea3adaf359570f2e211e0a9af74 (patch) | |
| tree | dd35bb9bff67f0622a6410c99b5bd1678c9c8299 /azalea-client/src/account | |
| parent | 7b84235a9be5bdc7c05873467ad8310b57448d79 (diff) | |
| download | azalea-drasl-1ca1f1d9e27aeea3adaf359570f2e211e0a9af74.tar.xz | |
Extensible Account (#301)
* refactor Account
* clean up implementation and docs
* add AccountTrait::join
* update changelog
* update example
Diffstat (limited to 'azalea-client/src/account')
| -rw-r--r-- | azalea-client/src/account/microsoft.rs | 279 | ||||
| -rw-r--r-- | azalea-client/src/account/mod.rs | 152 | ||||
| -rw-r--r-- | azalea-client/src/account/offline.rs | 37 |
3 files changed, 468 insertions, 0 deletions
diff --git a/azalea-client/src/account/microsoft.rs b/azalea-client/src/account/microsoft.rs new file mode 100644 index 00000000..22228eb9 --- /dev/null +++ b/azalea-client/src/account/microsoft.rs @@ -0,0 +1,279 @@ +use azalea_auth::{ + AccessTokenResponse, + certs::Certificates, + sessionserver::{self, ClientSessionServerError, SessionServerJoinOpts}, +}; +use parking_lot::Mutex; +use uuid::Uuid; + +use crate::account::{Account, AccountTrait, BoxFuture}; + +/// A type of account that authenticates with Microsoft using Azalea's cache. +/// +/// This type is not intended to be used directly by the user. To actually make +/// an account that authenticates with Microsoft, see [`Account::microsoft`]. +#[derive(Debug)] +pub struct MicrosoftAccount { + cache_key: String, + + username: String, + uuid: Uuid, + + access_token: Mutex<String>, + certs: Mutex<Option<Certificates>>, +} +impl MicrosoftAccount { + // deliberately private, use `Account::microsoft` or + // `Account::microsoft_with_custom_client_id_and_scope` instead. + async fn new( + cache_key: &str, + client_id: Option<&str>, + scope: Option<&str>, + ) -> Result<Self, azalea_auth::AuthError> { + let minecraft_dir = minecraft_folder_path::minecraft_dir().unwrap_or_else(|| { + panic!( + "No {} environment variable found", + minecraft_folder_path::home_env_var() + ) + }); + let auth_result = azalea_auth::auth( + cache_key, + azalea_auth::AuthOpts { + cache_file: Some(minecraft_dir.join("azalea-auth.json")), + client_id, + scope, + ..Default::default() + }, + ) + .await?; + + Ok(Self { + cache_key: cache_key.to_owned(), + username: auth_result.profile.name, + uuid: auth_result.profile.id, + access_token: Mutex::new(auth_result.access_token), + certs: Mutex::new(None), + }) + } +} +impl AccountTrait for MicrosoftAccount { + fn username(&self) -> &str { + &self.username + } + fn uuid(&self) -> Uuid { + self.uuid + } + fn access_token(&self) -> Option<String> { + Some(self.access_token.lock().to_owned()) + } + fn certs(&self) -> Option<azalea_auth::certs::Certificates> { + self.certs.lock().as_ref().cloned() + } + fn set_certs(&self, certs: azalea_auth::certs::Certificates) { + *self.certs.lock() = Some(certs); + } + fn refresh(&self) -> BoxFuture<'_, Result<(), azalea_auth::AuthError>> { + Box::pin(async { + let new_account = MicrosoftAccount::new(&self.cache_key, None, None).await?; + let new_access_token = new_account.access_token().unwrap(); + *self.access_token.lock() = new_access_token; + Ok(()) + }) + } + fn join<'a>( + &'a self, + public_key: &'a [u8], + private_key: &'a [u8; 16], + server_id: &'a str, + proxy: Option<reqwest::Proxy>, + ) -> BoxFuture<'a, Result<(), ClientSessionServerError>> { + Box::pin(async move { + let access_token = self.access_token.lock().clone(); + sessionserver::join(SessionServerJoinOpts { + access_token: &access_token, + public_key: public_key, + private_key: private_key, + uuid: &self.uuid(), + server_id: &server_id, + proxy, + }) + .await + }) + } +} + +/// A type of account that authenticates using a Microsoft access token that the +/// user directly passes. +/// +/// This does not use Azalea's account cache. +/// +/// This type is not intended to be used directly by the user. To actually make +/// an account that authenticates with Microsoft like this, see +/// [`Account::with_microsoft_access_token`]. +#[derive(Debug)] +pub struct MicrosoftWithAccessTokenAccount { + msa: Mutex<azalea_auth::cache::ExpiringValue<AccessTokenResponse>>, + + username: String, + uuid: Uuid, + + access_token: Mutex<String>, + certs: Mutex<Option<Certificates>>, +} +impl MicrosoftWithAccessTokenAccount { + async fn new( + msa: azalea_auth::cache::ExpiringValue<AccessTokenResponse>, + client_id: Option<&str>, + scope: Option<&str>, + ) -> Result<Self, azalea_auth::AuthError> { + let client = reqwest::Client::new(); + + let mut msa = msa.clone(); + + if msa.is_expired() { + use tracing::trace; + + trace!("refreshing Microsoft auth token"); + msa = azalea_auth::refresh_ms_auth_token( + &client, + &msa.data.refresh_token, + client_id, + scope, + ) + .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: Mutex::new(res.minecraft_access_token), + uuid: profile.id, + msa: Mutex::new(msa), + certs: Mutex::new(None), + }) + } +} +impl AccountTrait for MicrosoftWithAccessTokenAccount { + fn username(&self) -> &str { + &self.username + } + fn uuid(&self) -> Uuid { + self.uuid + } + fn access_token(&self) -> Option<String> { + Some(self.access_token.lock().to_owned()) + } + fn certs(&self) -> Option<azalea_auth::certs::Certificates> { + self.certs.lock().as_ref().cloned() + } + fn set_certs(&self, certs: azalea_auth::certs::Certificates) { + *self.certs.lock() = Some(certs); + } + fn refresh(&self) -> BoxFuture<'_, Result<(), azalea_auth::AuthError>> { + Box::pin(async { + let msa_value = self.msa.lock().clone(); + let new_account = MicrosoftWithAccessTokenAccount::new(msa_value, None, None).await?; + + let new_access_token = new_account.access_token().unwrap(); + + *self.access_token.lock() = new_access_token; + *self.msa.lock() = new_account.msa.lock().clone(); + + Ok(()) + }) + } + fn join<'a>( + &'a self, + public_key: &'a [u8], + private_key: &'a [u8; 16], + server_id: &'a str, + proxy: Option<reqwest::Proxy>, + ) -> BoxFuture<'a, Result<(), ClientSessionServerError>> { + Box::pin(async move { + let access_token = self.access_token.lock().clone(); + sessionserver::join(SessionServerJoinOpts { + access_token: &access_token, + public_key: public_key, + private_key: private_key, + uuid: &self.uuid(), + server_id: &server_id, + proxy, + }) + .await + }) + } +} + +impl Account { + /// This will create an online-mode account by authenticating with + /// Microsoft's servers. + /// + /// The cache key is used for avoiding having to log in every time. This is + /// typically set to the account email, but it can be any string. + #[cfg(feature = "online-mode")] + pub async fn microsoft(cache_key: &str) -> Result<Self, azalea_auth::AuthError> { + Self::microsoft_with_custom_client_id_and_scope(cache_key, None, None).await + } + + /// Similar to [`Account::microsoft`] but you can use your own `client_id` + /// and `scope`. + /// + /// Pass `None` if you want to use default ones. + #[cfg(feature = "online-mode")] + pub async fn microsoft_with_custom_client_id_and_scope( + cache_key: &str, + client_id: Option<&str>, + scope: Option<&str>, + ) -> Result<Self, azalea_auth::AuthError> { + MicrosoftAccount::new(cache_key, client_id, scope) + .await + .map(Account::from) + } + + /// 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). + /// + /// This will refresh the given token if it's expired. + /// + /// ``` + /// # 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, None, None).await?; + /// // Or, `azalea_auth::get_ms_link_code(&client, Some(client_id), None).await?` + /// // if you want to use your own client_id + /// println!( + /// "Go to {} and enter the code {}", + /// res.verification_uri, res.user_code + /// ); + /// let msa = azalea_auth::get_ms_auth_token(&client, res, None).await?; + /// Account::with_microsoft_access_token(msa).await?; + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "online-mode")] + pub async fn with_microsoft_access_token( + msa: azalea_auth::cache::ExpiringValue<AccessTokenResponse>, + ) -> Result<Self, azalea_auth::AuthError> { + Self::with_microsoft_access_token_and_custom_client_id_and_scope(msa, None, None).await + } + + /// Similar to [`Account::with_microsoft_access_token`] but you can use + /// custom `client_id` and `scope`. + #[cfg(feature = "online-mode")] + pub async fn with_microsoft_access_token_and_custom_client_id_and_scope( + msa: azalea_auth::cache::ExpiringValue<AccessTokenResponse>, + client_id: Option<&str>, + scope: Option<&str>, + ) -> Result<Self, azalea_auth::AuthError> { + MicrosoftWithAccessTokenAccount::new(msa, client_id, scope) + .await + .map(Account::from) + } +} diff --git a/azalea-client/src/account/mod.rs b/azalea-client/src/account/mod.rs new file mode 100644 index 00000000..22a099d1 --- /dev/null +++ b/azalea-client/src/account/mod.rs @@ -0,0 +1,152 @@ +//! Connect to Minecraft servers. + +#[cfg(feature = "online-mode")] +pub mod microsoft; +pub mod offline; + +use std::{fmt::Debug, ops::Deref, pin::Pin, sync::Arc}; + +#[cfg(feature = "online-mode")] +use azalea_auth::sessionserver::ClientSessionServerError; +use bevy_ecs::component::Component; +use uuid::Uuid; + +/// Something that can join Minecraft servers. +/// +/// By default, Azalea only supports either authentication with Microsoft +/// (online-mode), or no authentication at all (offline-mode). If you'd like to +/// do authentication in some other way, consider looking at [`AccountTrait`]. +/// +/// To join a server using this account, you can either use +/// [`StartJoinServerEvent`] or `azalea::ClientBuilder`. +/// +/// Note that `Account` is also an ECS component that's present on our client +/// entities. +/// +/// # Examples +/// +/// ```rust,no_run +/// # use azalea_client::Account; +/// # +/// # #[tokio::main] +/// # async fn main() { +/// let account = Account::microsoft("example@example.com").await; +/// // or Account::offline("example"); +/// # } +/// ``` +/// +/// [`StartJoinServerEvent`]: crate::join::StartJoinServerEvent +/// [`azalea::ClientBuilder`]: https://docs.rs/azalea/latest/azalea/struct.ClientBuilder.html +#[derive(Clone, Component, Debug)] +pub struct Account(Arc<dyn AccountTrait>); + +impl Account { + #[deprecated = "moved to `uuid()`."] + pub fn uuid_or_offline(&self) -> Uuid { + self.uuid() + } +} + +pub(crate) type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>; + +/// A trait that all types of accounts implement. +/// +/// This can be used, for example, to join servers with a custom authentication +/// server. +/// +/// Anything that implements [`AccountTrait`] can be converted to an [`Account`] +/// with `.into()`. +/// +/// Consider reading the source code of +/// [`MicrosoftAccount`](microsoft::MicrosoftAccount) for an example of how to +/// implement this. +pub trait AccountTrait: Send + Sync + Debug { + /// Returns the Minecraft username of the account. + fn username(&self) -> &str; + /// Returns the unique identifier for this player. + /// + /// For offline-mode accounts, this UUID is generated by calling + /// [`azalea_crypto::offline::generate_uuid`]. + fn uuid(&self) -> Uuid; + + /// The access token for authentication. + /// + /// You can obtain one of these manually from `azalea-auth`. + fn access_token(&self) -> Option<String>; + + /// Refreshes the access token for this account. + #[cfg(feature = "online-mode")] + fn refresh(&self) -> BoxFuture<'_, Result<(), azalea_auth::AuthError>> { + Box::pin(async { Ok(()) }) + } + /// Refreshes the access token for this account. + /// + /// The `online-mode` feature is disabled, so this won't do anything. + #[cfg(not(feature = "online-mode"))] + fn refresh(&self) -> BoxFuture<Result<(), ()>> { + Box::pin(async { Ok(()) }) + } + + #[cfg(feature = "online-mode")] + fn certs(&self) -> Option<azalea_auth::certs::Certificates> { + None + } + /// Override the chat signing certificates for this account. + /// + /// You can get the certificates needed for this from + /// [`azalea_auth::certs::fetch_certificates`]. You typically don't need to + /// call this yourself, as Azalea will do it for you. + /// + /// For accounts that don't support signing (i.e. offline-mode), this won't + /// do anything. + #[cfg(feature = "online-mode")] + fn set_certs(&self, certs: azalea_auth::certs::Certificates) { + let _ = certs; + } + + /// Typically used to tell Mojang's sessionserver that we are going to join + /// a server. + /// + /// This must be implemented for accounts that can join online-mode servers. + /// + /// This function is called internally by Azalea when the account tries to + /// join a server, but only if [`AccountTrait::access_token`] is `Some`. + #[cfg(feature = "online-mode")] + fn join<'a>( + &'a self, + public_key: &'a [u8], + private_key: &'a [u8; 16], + server_id: &'a str, + proxy: Option<reqwest::Proxy>, + ) -> BoxFuture<'a, Result<(), ClientSessionServerError>> { + let _ = (public_key, private_key, server_id, proxy); + Box::pin(async { Ok(()) }) + } + /// Typically used to tell Mojang's sessionserver that we are going to join + /// a server. + /// + /// The `online-mode` feature is disabled, so this won't do anything. + #[cfg(not(feature = "online-mode"))] + fn join( + &self, + public_key: &[u8], + private_key: &[u8; 16], + server_id: &str, + proxy: Option<reqwest::Proxy>, + ) -> BoxFuture<Result<(), ()>> { + let _ = (public_key, private_key, server_id, proxy); + Box::pin(async { Ok(()) }) + } +} +impl<T: AccountTrait + 'static> From<T> for Account { + fn from(value: T) -> Self { + Account(Arc::new(value)) + } +} +impl Deref for Account { + type Target = dyn AccountTrait; + + fn deref(&self) -> &Self::Target { + &*self.0 + } +} diff --git a/azalea-client/src/account/offline.rs b/azalea-client/src/account/offline.rs new file mode 100644 index 00000000..f0ee49bd --- /dev/null +++ b/azalea-client/src/account/offline.rs @@ -0,0 +1,37 @@ +use uuid::Uuid; + +use crate::account::{Account, AccountTrait}; + +/// A type of account that does not perform any authentication and cannot join +/// online-mode servers. +/// +/// This type is not intended to be used directly by the user. To actually make +/// an offline-mode account, see [`Account::offline`]. +#[derive(Debug)] +pub struct OfflineAccount { + username: String, +} +impl AccountTrait for OfflineAccount { + fn username(&self) -> &str { + &self.username + } + fn uuid(&self) -> Uuid { + azalea_crypto::offline::generate_uuid(&self.username) + } + fn access_token(&self) -> Option<String> { + None + } +} + +impl Account { + /// An offline account does not authenticate with Microsoft's servers, and + /// as such can only join offline mode servers. + /// + /// This is useful for testing in LAN worlds. + pub fn offline(username: &str) -> Self { + OfflineAccount { + username: username.to_owned(), + } + .into() + } +} |
