diff --git a/azalea-auth/examples/auth_manual.rs b/azalea-auth/examples/auth_manual.rs index 6a9510a8f..05803c875 100755 --- a/azalea-auth/examples/auth_manual.rs +++ b/azalea-auth/examples/auth_manual.rs @@ -18,15 +18,16 @@ async fn main() -> Result<(), Box> { Ok(()) } +// We will be using default `client_id` and `scope` async fn auth() -> Result> { let client = reqwest::Client::new(); - let res = azalea_auth::get_ms_link_code(&client).await?; + let res = azalea_auth::get_ms_link_code(&client, None, None).await?; println!( "Go to {} and enter the code {}", res.verification_uri, res.user_code ); - let msa = azalea_auth::get_ms_auth_token(&client, res).await?; + let msa = azalea_auth::get_ms_auth_token(&client, res, None).await?; let auth_result = azalea_auth::get_minecraft_token(&client, &msa.data.access_token).await?; Ok(azalea_auth::get_profile(&client, &auth_result.minecraft_access_token).await?) } diff --git a/azalea-auth/src/auth.rs b/azalea-auth/src/auth.rs index e0a75adff..08d2d3a84 100755 --- a/azalea-auth/src/auth.rs +++ b/azalea-auth/src/auth.rs @@ -13,7 +13,7 @@ use thiserror::Error; use uuid::Uuid; #[derive(Default)] -pub struct AuthOpts { +pub struct AuthOpts<'a> { /// Whether we should check if the user actually owns the game. This will /// fail if the user has Xbox Game Pass! Note that this isn't really /// necessary, since getting the user profile will check this anyways. @@ -24,6 +24,12 @@ pub struct AuthOpts { /// The directory to store the cache in. If this is not set, caching is not /// done. pub cache_file: Option, + /// If you choose to use your own Microsoft authentication instead of using + /// Nintendo Switch, just put your client_id here. + pub client_id: Option<&'a str>, + /// If you want to use custom scope instead of default one, just put your + /// scope here. + pub scope: Option<&'a str>, } #[derive(Debug, Error)] @@ -59,7 +65,7 @@ pub enum AuthError { /// If you want to use your own code to cache or show the auth code to the user /// in a different way, use [`get_ms_link_code`], [`get_ms_auth_token`], /// [`get_minecraft_token`] and [`get_profile`] instead. -pub async fn auth(email: &str, opts: AuthOpts) -> Result { +pub async fn auth<'a>(email: &str, opts: AuthOpts<'a>) -> Result { let cached_account = if let Some(cache_file) = &opts.cache_file { cache::get_account_in_cache(cache_file, email).await } else { @@ -76,20 +82,32 @@ pub async fn auth(email: &str, opts: AuthOpts) -> Result profile: account.profile.clone(), }) } else { + let client_id = opts.client_id.unwrap_or(CLIENT_ID); + let scope = opts.scope.unwrap_or(SCOPE); + let client = reqwest::Client::new(); let mut msa = if let Some(account) = cached_account { account.msa } else { - interactive_get_ms_auth_token(&client, email).await? + interactive_get_ms_auth_token(&client, email, Some(client_id), Some(scope)).await? }; if msa.is_expired() { tracing::trace!("refreshing Microsoft auth token"); - match refresh_ms_auth_token(&client, &msa.data.refresh_token).await { + match refresh_ms_auth_token( + &client, + &msa.data.refresh_token, + opts.client_id, + opts.scope, + ) + .await + { Ok(new_msa) => msa = new_msa, Err(e) => { // can't refresh, ask the user to auth again tracing::error!("Error refreshing Microsoft auth token: {}", e); - msa = interactive_get_ms_auth_token(&client, email).await?; + msa = + interactive_get_ms_auth_token(&client, email, Some(client_id), Some(scope)) + .await?; } } } @@ -259,6 +277,7 @@ pub struct ProfileResponse { // nintendo switch (so it works for accounts that are under 18 years old) const CLIENT_ID: &str = "00000000441cc96b"; +const SCOPE: &str = "service::user.auth.xboxlive.com::MBI_SSL"; #[derive(Debug, Error)] pub enum GetMicrosoftAuthTokenError { @@ -280,12 +299,12 @@ pub enum GetMicrosoftAuthTokenError { /// /// ``` /// # async fn example(client: &reqwest::Client) -> Result<(), Box> { -/// let res = azalea_auth::get_ms_link_code(&client).await?; +/// let res = azalea_auth::get_ms_link_code(&client, None, None).await?; /// println!( /// "Go to {} and enter the code {}", /// res.verification_uri, res.user_code /// ); -/// let msa = azalea_auth::get_ms_auth_token(client, res).await?; +/// let msa = azalea_auth::get_ms_auth_token(client, res, None).await?; /// let minecraft = azalea_auth::get_minecraft_token(client, &msa.data.access_token).await?; /// let profile = azalea_auth::get_profile(&client, &minecraft.minecraft_access_token).await?; /// # Ok(()) @@ -293,12 +312,22 @@ pub enum GetMicrosoftAuthTokenError { /// ``` pub async fn get_ms_link_code( client: &reqwest::Client, + client_id: Option<&str>, + scope: Option<&str>, ) -> Result { + let client_id = if let Some(c) = client_id { + c + } else { + CLIENT_ID + }; + + let scope = if let Some(c) = scope { c } else { SCOPE }; + Ok(client .post("https://login.live.com/oauth20_connect.srf") .form(&vec![ - ("scope", "service::user.auth.xboxlive.com::MBI_SSL"), - ("client_id", CLIENT_ID), + ("scope", scope), + ("client_id", client_id), ("response_type", "device_code"), ]) .send() @@ -314,7 +343,14 @@ pub async fn get_ms_link_code( pub async fn get_ms_auth_token( client: &reqwest::Client, res: DeviceCodeResponse, + client_id: Option<&str>, ) -> Result, GetMicrosoftAuthTokenError> { + let client_id = if let Some(c) = client_id { + c + } else { + CLIENT_ID + }; + let login_expires_at = Instant::now() + std::time::Duration::from_secs(res.expires_in); while Instant::now() < login_expires_at { @@ -323,10 +359,10 @@ pub async fn get_ms_auth_token( tracing::trace!("Polling to check if user has logged in..."); if let Ok(access_token_response) = client .post(format!( - "https://login.live.com/oauth20_token.srf?client_id={CLIENT_ID}" + "https://login.live.com/oauth20_token.srf?client_id={client_id}" )) .form(&vec![ - ("client_id", CLIENT_ID), + ("client_id", client_id), ("device_code", &res.device_code), ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"), ]) @@ -357,15 +393,17 @@ pub async fn get_ms_auth_token( pub async fn interactive_get_ms_auth_token( client: &reqwest::Client, email: &str, + client_id: Option<&str>, + scope: Option<&str>, ) -> Result, GetMicrosoftAuthTokenError> { - let res = get_ms_link_code(client).await?; + let res = get_ms_link_code(client, client_id, scope).await?; tracing::trace!("Device code response: {:?}", res); println!( "Go to \x1b[1m{}\x1b[m and enter the code \x1b[1m{}\x1b[m for \x1b[1m{}\x1b[m", res.verification_uri, res.user_code, email ); - get_ms_auth_token(client, res).await + get_ms_auth_token(client, res, client_id).await } #[derive(Debug, Error)] @@ -379,12 +417,17 @@ pub enum RefreshMicrosoftAuthTokenError { pub async fn refresh_ms_auth_token( client: &reqwest::Client, refresh_token: &str, + client_id: Option<&str>, + scope: Option<&str>, ) -> Result, RefreshMicrosoftAuthTokenError> { + let client_id = client_id.unwrap_or(CLIENT_ID); + let scope = scope.unwrap_or(SCOPE); + let access_token_response_text = client .post("https://login.live.com/oauth20_token.srf") .form(&vec![ - ("scope", "service::user.auth.xboxlive.com::MBI_SSL"), - ("client_id", CLIENT_ID), + ("scope", scope), + ("client_id", client_id), ("grant_type", "refresh_token"), ("refresh_token", refresh_token), ]) diff --git a/azalea-client/src/account.rs b/azalea-client/src/account.rs index 741a07d45..c007e01ab 100755 --- a/azalea-client/src/account.rs +++ b/azalea-client/src/account.rs @@ -90,6 +90,18 @@ impl Account { /// a key for the cache, but it's recommended to use the real email to /// avoid confusion. pub async fn microsoft(email: &str) -> Result { + Self::microsoft_with_custom_client_id_and_scope(email, None, None).await + } + + /// Similar to [`account.microsoft()`](Self::microsoft) but you can use your + /// own `client_id` and `scope`. + /// + /// Pass `None` if you want to use default ones. + pub async fn microsoft_with_custom_client_id_and_scope( + email: &str, + client_id: Option<&str>, + scope: Option<&str>, + ) -> Result { let minecraft_dir = minecraft_folder_path::minecraft_dir().unwrap_or_else(|| { panic!( "No {} environment variable found", @@ -100,6 +112,8 @@ impl Account { email, azalea_auth::AuthOpts { cache_file: Some(minecraft_dir.join("azalea-auth.json")), + client_id, + scope, ..Default::default() }, ) @@ -128,24 +142,42 @@ impl Account { /// # async fn example() -> Result<(), Box> { /// let client = reqwest::Client::new(); /// - /// let res = azalea_auth::get_ms_link_code(&client).await?; + /// 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).await?; + /// let msa = azalea_auth::get_ms_auth_token(&client, res, None).await?; /// Account::with_microsoft_access_token(msa).await?; /// # Ok(()) /// # } /// ``` pub async fn with_microsoft_access_token( + msa: azalea_auth::cache::ExpiringValue, + ) -> Result { + 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`. + pub async fn with_microsoft_access_token_and_custom_client_id_and_scope( mut msa: azalea_auth::cache::ExpiringValue, + client_id: Option<&str>, + scope: Option<&str>, ) -> Result { let client = reqwest::Client::new(); if msa.is_expired() { tracing::trace!("refreshing Microsoft auth token"); - msa = azalea_auth::refresh_ms_auth_token(&client, &msa.data.refresh_token).await?; + msa = azalea_auth::refresh_ms_auth_token( + &client, + &msa.data.refresh_token, + client_id, + scope, + ) + .await?; } let msa_token = &msa.data.access_token;