azalea_client/
account.rs

1//! Connect to Minecraft servers.
2
3use std::sync::Arc;
4
5use azalea_auth::certs::{Certificates, FetchCertificatesError};
6use azalea_auth::AccessTokenResponse;
7use bevy_ecs::component::Component;
8use parking_lot::Mutex;
9use thiserror::Error;
10use tracing::trace;
11use uuid::Uuid;
12
13/// Something that can join Minecraft servers.
14///
15/// To join a server using this account, use [`Client::join`] or
16/// [`azalea::ClientBuilder`].
17///
18/// Note that this is also a component that our clients have.
19///
20/// # Examples
21///
22/// ```rust,no_run
23/// use azalea_client::Account;
24///
25/// # #[tokio::main]
26/// # async fn main() {
27/// let account = Account::microsoft("[email protected]").await;
28/// // or Account::offline("example");
29/// # }
30/// ```
31///
32/// [`Client::join`]: crate::Client::join
33/// [`azalea::ClientBuilder`]: https://docs.rs/azalea/latest/azalea/struct.ClientBuilder.html
34#[derive(Clone, Debug, Component)]
35pub struct Account {
36    /// The Minecraft username of the account.
37    pub username: String,
38    /// The access token for authentication. You can obtain one of these
39    /// manually from azalea-auth.
40    ///
41    /// This is an `Arc<Mutex>` so it can be modified by [`Self::refresh`].
42    pub access_token: Option<Arc<Mutex<String>>>,
43    /// Only required for online-mode accounts.
44    pub uuid: Option<Uuid>,
45
46    /// The parameters (i.e. email) that were passed for creating this
47    /// [`Account`]. This is used for automatic reauthentication when we get
48    /// "Invalid Session" errors. If you don't need that feature (like in
49    /// offline mode), then you can set this to `AuthOpts::default()`.
50    pub account_opts: AccountOpts,
51
52    /// The certificates used for chat signing.
53    ///
54    /// This is set when you call [`Self::request_certs`], but you only
55    /// need to if the servers you're joining require it.
56    pub certs: Option<Certificates>,
57}
58
59/// The parameters that were passed for creating the associated [`Account`].
60#[derive(Clone, Debug)]
61pub enum AccountOpts {
62    Offline {
63        username: String,
64    },
65    Microsoft {
66        email: String,
67    },
68    MicrosoftWithAccessToken {
69        msa: Arc<Mutex<azalea_auth::cache::ExpiringValue<AccessTokenResponse>>>,
70    },
71}
72
73impl Account {
74    /// An offline account does not authenticate with Microsoft's servers, and
75    /// as such can only join offline mode servers. This is useful for testing
76    /// in LAN worlds.
77    pub fn offline(username: &str) -> Self {
78        Self {
79            username: username.to_string(),
80            access_token: None,
81            uuid: None,
82            account_opts: AccountOpts::Offline {
83                username: username.to_string(),
84            },
85            certs: None,
86        }
87    }
88
89    /// This will create an online-mode account by authenticating with
90    /// Microsoft's servers. Note that the email given is actually only used as
91    /// a key for the cache, but it's recommended to use the real email to
92    /// avoid confusion.
93    pub async fn microsoft(email: &str) -> Result<Self, azalea_auth::AuthError> {
94        Self::microsoft_with_custom_client_id_and_scope(email, None, None).await
95    }
96
97    /// Similar to [`Account::microsoft`] but you can use your
98    /// own `client_id` and `scope`.
99    ///
100    /// Pass `None` if you want to use default ones.
101    pub async fn microsoft_with_custom_client_id_and_scope(
102        email: &str,
103        client_id: Option<&str>,
104        scope: Option<&str>,
105    ) -> Result<Self, azalea_auth::AuthError> {
106        let minecraft_dir = minecraft_folder_path::minecraft_dir().unwrap_or_else(|| {
107            panic!(
108                "No {} environment variable found",
109                minecraft_folder_path::home_env_var()
110            )
111        });
112        let auth_result = azalea_auth::auth(
113            email,
114            azalea_auth::AuthOpts {
115                cache_file: Some(minecraft_dir.join("azalea-auth.json")),
116                client_id,
117                scope,
118                ..Default::default()
119            },
120        )
121        .await?;
122        Ok(Self {
123            username: auth_result.profile.name,
124            access_token: Some(Arc::new(Mutex::new(auth_result.access_token))),
125            uuid: Some(auth_result.profile.id),
126            account_opts: AccountOpts::Microsoft {
127                email: email.to_string(),
128            },
129            // we don't do chat signing by default unless the user asks for it
130            certs: None,
131        })
132    }
133
134    /// This will create an online-mode account through
135    /// [`azalea_auth::get_minecraft_token`] so you can have more control over
136    /// the authentication process (like doing your own caching or
137    /// displaying the Microsoft user code to the user in a different way).
138    ///
139    /// This will refresh the given token if it's expired.
140    ///
141    /// ```
142    /// # use azalea_client::Account;
143    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
144    /// let client = reqwest::Client::new();
145    ///
146    /// let res = azalea_auth::get_ms_link_code(&client, None, None).await?;
147    /// // Or, `azalea_auth::get_ms_link_code(&client, Some(client_id), None).await?`
148    /// // if you want to use your own client_id
149    /// println!(
150    ///     "Go to {} and enter the code {}",
151    ///     res.verification_uri, res.user_code
152    /// );
153    /// let msa = azalea_auth::get_ms_auth_token(&client, res, None).await?;
154    /// Account::with_microsoft_access_token(msa).await?;
155    /// # Ok(())
156    /// # }
157    /// ```
158    pub async fn with_microsoft_access_token(
159        msa: azalea_auth::cache::ExpiringValue<AccessTokenResponse>,
160    ) -> Result<Self, azalea_auth::AuthError> {
161        Self::with_microsoft_access_token_and_custom_client_id_and_scope(msa, None, None).await
162    }
163
164    /// Similar to [`Account::with_microsoft_access_token`] but you can use
165    /// custom `client_id` and `scope`.
166    pub async fn with_microsoft_access_token_and_custom_client_id_and_scope(
167        mut msa: azalea_auth::cache::ExpiringValue<AccessTokenResponse>,
168        client_id: Option<&str>,
169        scope: Option<&str>,
170    ) -> Result<Self, azalea_auth::AuthError> {
171        let client = reqwest::Client::new();
172
173        if msa.is_expired() {
174            trace!("refreshing Microsoft auth token");
175            msa = azalea_auth::refresh_ms_auth_token(
176                &client,
177                &msa.data.refresh_token,
178                client_id,
179                scope,
180            )
181            .await?;
182        }
183
184        let msa_token = &msa.data.access_token;
185
186        let res = azalea_auth::get_minecraft_token(&client, msa_token).await?;
187
188        let profile = azalea_auth::get_profile(&client, &res.minecraft_access_token).await?;
189
190        Ok(Self {
191            username: profile.name,
192            access_token: Some(Arc::new(Mutex::new(res.minecraft_access_token))),
193            uuid: Some(profile.id),
194            account_opts: AccountOpts::MicrosoftWithAccessToken {
195                msa: Arc::new(Mutex::new(msa)),
196            },
197            certs: None,
198        })
199    }
200    /// Refresh the access_token for this account to be valid again.
201    ///
202    /// This requires the `auth_opts` field to be set correctly (which is done
203    /// by default if you used the constructor functions). Note that if the
204    /// Account is offline-mode then this function won't do anything.
205    pub async fn refresh(&self) -> Result<(), azalea_auth::AuthError> {
206        match &self.account_opts {
207            // offline mode doesn't need to refresh so just don't do anything lol
208            AccountOpts::Offline { .. } => Ok(()),
209            AccountOpts::Microsoft { email } => {
210                let new_account = Account::microsoft(email).await?;
211                let access_token_mutex = self.access_token.as_ref().unwrap();
212                let new_access_token = new_account.access_token.unwrap().lock().clone();
213                *access_token_mutex.lock() = new_access_token;
214                Ok(())
215            }
216            AccountOpts::MicrosoftWithAccessToken { msa } => {
217                let msa_value = msa.lock().clone();
218                let new_account = Account::with_microsoft_access_token(msa_value).await?;
219
220                let access_token_mutex = self.access_token.as_ref().unwrap();
221                let new_access_token = new_account.access_token.unwrap().lock().clone();
222
223                *access_token_mutex.lock() = new_access_token;
224                let AccountOpts::MicrosoftWithAccessToken { msa: new_msa } =
225                    new_account.account_opts
226                else {
227                    unreachable!()
228                };
229                *msa.lock() = new_msa.lock().clone();
230
231                Ok(())
232            }
233        }
234    }
235
236    /// Get the UUID of this account. This will generate an offline-mode UUID
237    /// by making a hash with the username if the `uuid` field is None.
238    pub fn uuid_or_offline(&self) -> Uuid {
239        self.uuid
240            .unwrap_or_else(|| azalea_auth::offline::generate_uuid(&self.username))
241    }
242}
243
244#[derive(Error, Debug)]
245pub enum RequestCertError {
246    #[error("Failed to fetch certificates")]
247    FetchCertificates(#[from] FetchCertificatesError),
248    #[error("You can't request certificates for an offline account")]
249    NoAccessToken,
250}
251
252impl Account {
253    /// Request the certificates used for chat signing and set it in
254    /// [`Self::certs`].
255    pub async fn request_certs(&mut self) -> Result<(), RequestCertError> {
256        let access_token = self
257            .access_token
258            .as_ref()
259            .ok_or(RequestCertError::NoAccessToken)?
260            .lock()
261            .clone();
262        let certs = azalea_auth::certs::fetch_certificates(&access_token).await?;
263        self.certs = Some(certs);
264
265        Ok(())
266    }
267}