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}