azalea_client/account/
microsoft.rs

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