Skip to main content

azalea_client/account/
microsoft.rs

1use std::path::PathBuf;
2
3use azalea_auth::{
4    AccessTokenResponse, AuthOpts,
5    certs::Certificates,
6    sessionserver::{self, ClientSessionServerError, SessionServerJoinOpts},
7};
8use parking_lot::Mutex;
9use uuid::Uuid;
10
11use crate::account::{Account, AccountTrait, BoxFuture};
12
13fn default_cache_file() -> PathBuf {
14    let minecraft_dir = minecraft_folder_path::minecraft_dir().unwrap_or_else(|| {
15        panic!(
16            "No {} environment variable found",
17            minecraft_folder_path::home_env_var()
18        )
19    });
20    minecraft_dir.join("azalea-auth.json")
21}
22
23/// Options for Microsoft authentication in Azalea.
24///
25/// This is used by [`Account::microsoft_with_opts`].
26#[derive(Clone, Debug, Default)]
27pub struct MicrosoftAccountOpts {
28    /// Whether we should check if the user owns the game.
29    pub check_ownership: bool,
30    /// The cache file to use for the auth cache.
31    ///
32    /// If this is `None`, Azalea will default to its standard cache file
33    /// (`~/.minecraft/azalea-auth.json`).
34    pub cache_file: Option<PathBuf>,
35    /// An override for the Microsoft Client ID to authenticate with.
36    pub client_id: Option<String>,
37    /// An override for the OAuth2 scope to authenticate with.
38    pub scope: Option<String>,
39}
40
41impl MicrosoftAccountOpts {
42    fn to_auth_opts(&self) -> AuthOpts<'_> {
43        let cache_file = self
44            .cache_file
45            .clone()
46            .or_else(|| Some(default_cache_file()));
47
48        AuthOpts {
49            check_ownership: self.check_ownership,
50            cache_file,
51            client_id: self.client_id.as_deref(),
52            scope: self.scope.as_deref(),
53        }
54    }
55}
56
57fn default_account_opts(client_id: Option<&str>, scope: Option<&str>) -> MicrosoftAccountOpts {
58    MicrosoftAccountOpts {
59        check_ownership: false,
60        cache_file: Some(default_cache_file()),
61        client_id: client_id.map(str::to_owned),
62        scope: scope.map(str::to_owned),
63    }
64}
65
66/// A type of account that authenticates with Microsoft using Azalea's cache.
67///
68/// This type is not intended to be used directly by the user. To actually make
69/// an account that authenticates with Microsoft, see [`Account::microsoft`] or
70/// [`Account::microsoft_with_opts`].
71#[derive(Debug)]
72pub struct MicrosoftAccount {
73    cache_key: String,
74    auth_opts: MicrosoftAccountOpts,
75
76    username: String,
77    uuid: Uuid,
78
79    access_token: Mutex<String>,
80    certs: Mutex<Option<Certificates>>,
81}
82impl MicrosoftAccount {
83    // deliberately private, use `Account::microsoft` or
84    // `Account::microsoft_with_opts` instead.
85    async fn new(
86        cache_key: &str,
87        auth_opts: MicrosoftAccountOpts,
88    ) -> Result<Self, azalea_auth::AuthError> {
89        let auth_result = azalea_auth::auth(cache_key, auth_opts.to_auth_opts()).await?;
90
91        Ok(Self {
92            cache_key: cache_key.to_owned(),
93            auth_opts,
94            username: auth_result.profile.name,
95            uuid: auth_result.profile.id,
96            access_token: Mutex::new(auth_result.access_token),
97            certs: Mutex::new(None),
98        })
99    }
100}
101impl AccountTrait for MicrosoftAccount {
102    fn username(&self) -> &str {
103        &self.username
104    }
105    fn uuid(&self) -> Uuid {
106        self.uuid
107    }
108    fn access_token(&self) -> Option<String> {
109        Some(self.access_token.lock().to_owned())
110    }
111    fn certs(&self) -> Option<azalea_auth::certs::Certificates> {
112        self.certs.lock().as_ref().cloned()
113    }
114    fn set_certs(&self, certs: azalea_auth::certs::Certificates) {
115        *self.certs.lock() = Some(certs);
116    }
117    fn refresh(&self) -> BoxFuture<'_, Result<(), azalea_auth::AuthError>> {
118        Box::pin(async {
119            let new_account =
120                MicrosoftAccount::new(&self.cache_key, self.auth_opts.clone()).await?;
121            let new_access_token = new_account.access_token().unwrap();
122            *self.access_token.lock() = new_access_token;
123            Ok(())
124        })
125    }
126    fn join<'a>(
127        &'a self,
128        public_key: &'a [u8],
129        private_key: &'a [u8; 16],
130        server_id: &'a str,
131        proxy: Option<reqwest::Proxy>,
132    ) -> BoxFuture<'a, Result<(), ClientSessionServerError>> {
133        Box::pin(async move {
134            let access_token = self.access_token.lock().clone();
135            sessionserver::join(SessionServerJoinOpts {
136                access_token: &access_token,
137                public_key,
138                private_key,
139                uuid: &self.uuid(),
140                server_id,
141                proxy,
142            })
143            .await
144        })
145    }
146}
147
148/// A type of account that authenticates using a Microsoft access token that the
149/// user directly passes.
150///
151/// This does not use Azalea's account cache.
152///
153/// This type is not intended to be used directly by the user. To actually make
154/// an account that authenticates with Microsoft like this, see
155/// [`Account::with_microsoft_access_token`].
156#[derive(Debug)]
157pub struct MicrosoftWithAccessTokenAccount {
158    msa: Mutex<azalea_auth::cache::ExpiringValue<AccessTokenResponse>>,
159
160    username: String,
161    uuid: Uuid,
162
163    access_token: Mutex<String>,
164    certs: Mutex<Option<Certificates>>,
165}
166impl MicrosoftWithAccessTokenAccount {
167    async fn new(
168        msa: azalea_auth::cache::ExpiringValue<AccessTokenResponse>,
169        client_id: Option<&str>,
170        scope: Option<&str>,
171    ) -> Result<Self, azalea_auth::AuthError> {
172        let client = reqwest::Client::new();
173
174        let mut msa = msa.clone();
175
176        if msa.is_expired() {
177            use tracing::trace;
178
179            trace!("refreshing Microsoft auth token");
180            msa = azalea_auth::refresh_ms_auth_token(
181                &client,
182                &msa.data.refresh_token,
183                client_id,
184                scope,
185            )
186            .await?;
187        }
188
189        let msa_token = &msa.data.access_token;
190        let res = azalea_auth::get_minecraft_token(&client, msa_token).await?;
191        let profile = azalea_auth::get_profile(&client, &res.minecraft_access_token).await?;
192
193        Ok(Self {
194            username: profile.name,
195            access_token: Mutex::new(res.minecraft_access_token),
196            uuid: profile.id,
197            msa: Mutex::new(msa),
198            certs: Mutex::new(None),
199        })
200    }
201}
202impl AccountTrait for MicrosoftWithAccessTokenAccount {
203    fn username(&self) -> &str {
204        &self.username
205    }
206    fn uuid(&self) -> Uuid {
207        self.uuid
208    }
209    fn access_token(&self) -> Option<String> {
210        Some(self.access_token.lock().to_owned())
211    }
212    fn certs(&self) -> Option<azalea_auth::certs::Certificates> {
213        self.certs.lock().as_ref().cloned()
214    }
215    fn set_certs(&self, certs: azalea_auth::certs::Certificates) {
216        *self.certs.lock() = Some(certs);
217    }
218    fn refresh(&self) -> BoxFuture<'_, Result<(), azalea_auth::AuthError>> {
219        Box::pin(async {
220            let msa_value = self.msa.lock().clone();
221            let new_account = MicrosoftWithAccessTokenAccount::new(msa_value, None, None).await?;
222
223            let new_access_token = new_account.access_token().unwrap();
224
225            *self.access_token.lock() = new_access_token;
226            *self.msa.lock() = new_account.msa.lock().clone();
227
228            Ok(())
229        })
230    }
231    fn join<'a>(
232        &'a self,
233        public_key: &'a [u8],
234        private_key: &'a [u8; 16],
235        server_id: &'a str,
236        proxy: Option<reqwest::Proxy>,
237    ) -> BoxFuture<'a, Result<(), ClientSessionServerError>> {
238        Box::pin(async move {
239            let access_token = self.access_token.lock().clone();
240            sessionserver::join(SessionServerJoinOpts {
241                access_token: &access_token,
242                public_key,
243                private_key,
244                uuid: &self.uuid(),
245                server_id,
246                proxy,
247            })
248            .await
249        })
250    }
251}
252
253impl Account {
254    /// This will create an online-mode account by authenticating with
255    /// Microsoft's servers.
256    ///
257    /// The cache key is used for avoiding having to log in every time. This is
258    /// typically set to the account email, but it can be any string.
259    #[cfg(feature = "online-mode")]
260    pub async fn microsoft(cache_key: &str) -> Result<Self, azalea_auth::AuthError> {
261        MicrosoftAccount::new(cache_key, default_account_opts(None, None))
262            .await
263            .map(Account::from)
264    }
265
266    /// Similar to [`Account::microsoft`] but you can pass custom auth options
267    /// (including the cache file location).
268    ///
269    /// For a custom cache directory, set
270    /// `auth_opts.cache_file = Some(custom_dir.join("azalea-auth.json"))`.
271    ///
272    /// If `auth_opts.cache_file` is `None`, it will default to Azalea's
273    /// standard cache file (`~/.minecraft/azalea-auth.json`) to match
274    /// [`Account::microsoft`].
275    #[cfg(feature = "online-mode")]
276    pub async fn microsoft_with_opts(
277        cache_key: &str,
278        auth_opts: MicrosoftAccountOpts,
279    ) -> Result<Self, azalea_auth::AuthError> {
280        MicrosoftAccount::new(cache_key, auth_opts)
281            .await
282            .map(Account::from)
283    }
284
285    /// Similar to [`Account::microsoft`] but you can use your own `client_id`
286    /// and `scope`.
287    ///
288    /// Pass `None` if you want to use default ones.
289    #[cfg(feature = "online-mode")]
290    #[deprecated(note = "Use `Account::microsoft_with_opts` instead.")]
291    pub async fn microsoft_with_custom_client_id_and_scope(
292        cache_key: &str,
293        client_id: Option<&str>,
294        scope: Option<&str>,
295    ) -> Result<Self, azalea_auth::AuthError> {
296        MicrosoftAccount::new(cache_key, default_account_opts(client_id, scope))
297            .await
298            .map(Account::from)
299    }
300
301    /// This will create an online-mode account through
302    /// [`azalea_auth::get_minecraft_token`] so you can have more control over
303    /// the authentication process (like doing your own caching or
304    /// displaying the Microsoft user code to the user in a different way).
305    ///
306    /// This will refresh the given token if it's expired.
307    ///
308    /// ```
309    /// # use azalea_client::Account;
310    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
311    /// let client = reqwest::Client::new();
312    ///
313    /// let res = azalea_auth::get_ms_link_code(&client, None, None).await?;
314    /// // Or, `azalea_auth::get_ms_link_code(&client, Some(client_id), None).await?`
315    /// // if you want to use your own client_id
316    /// println!(
317    ///     "Go to {} and enter the code {}",
318    ///     res.verification_uri, res.user_code
319    /// );
320    /// let msa = azalea_auth::get_ms_auth_token(&client, res, None).await?;
321    /// Account::with_microsoft_access_token(msa).await?;
322    /// # Ok(())
323    /// # }
324    /// ```
325    #[cfg(feature = "online-mode")]
326    pub async fn with_microsoft_access_token(
327        msa: azalea_auth::cache::ExpiringValue<AccessTokenResponse>,
328    ) -> Result<Self, azalea_auth::AuthError> {
329        Self::with_microsoft_access_token_and_custom_client_id_and_scope(msa, None, None).await
330    }
331
332    /// Similar to [`Account::with_microsoft_access_token`] but you can use
333    /// custom `client_id` and `scope`.
334    #[cfg(feature = "online-mode")]
335    pub async fn with_microsoft_access_token_and_custom_client_id_and_scope(
336        msa: azalea_auth::cache::ExpiringValue<AccessTokenResponse>,
337        client_id: Option<&str>,
338        scope: Option<&str>,
339    ) -> Result<Self, azalea_auth::AuthError> {
340        MicrosoftWithAccessTokenAccount::new(msa, client_id, scope)
341            .await
342            .map(Account::from)
343    }
344}