Skip to content

Commit

Permalink
Auth Customization Options (#159)
Browse files Browse the repository at this point in the history
* Added Support for Custom Auth using `client_id` and `scope`

* fix: `Account::microsoft` and added lifetime to `Account::microsoft_with_custom_client_id`

* Added function `with_microsoft_access_token_and_custom_client_id`

* Removed Custom Scope.

* I got carried away, and made scope also customizable, later realized no customization is needed.

* Better Documentation and Minor fixes

* Added Custom Scope

* Added RpsTicket format for custom `client_id`

* Moved to non-static str

* fix example

Co-authored-by: mat <[email protected]>

* fix doc grammer

* changed function signature

* fmt

* fixed example

* removed dead code

* Removed `d=` insertion in `client_id`

* removed unnecessary `mut`

---------

Co-authored-by: mat <[email protected]>
  • Loading branch information
AS1100K and mat-1 authored Aug 11, 2024
1 parent 92c9075 commit 13afc1d
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 20 deletions.
5 changes: 3 additions & 2 deletions azalea-auth/examples/auth_manual.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,16 @@ async fn main() -> Result<(), Box<dyn Error>> {
Ok(())
}

// We will be using default `client_id` and `scope`
async fn auth() -> Result<ProfileResponse, Box<dyn Error>> {
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?)
}
73 changes: 58 additions & 15 deletions azalea-auth/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<PathBuf>,
/// 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)]
Expand Down Expand Up @@ -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<AuthResult, AuthError> {
pub async fn auth<'a>(email: &str, opts: AuthOpts<'a>) -> Result<AuthResult, AuthError> {
let cached_account = if let Some(cache_file) = &opts.cache_file {
cache::get_account_in_cache(cache_file, email).await
} else {
Expand All @@ -76,20 +82,32 @@ pub async fn auth(email: &str, opts: AuthOpts) -> Result<AuthResult, AuthError>
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?;
}
}
}
Expand Down Expand Up @@ -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 {
Expand All @@ -280,25 +299,35 @@ pub enum GetMicrosoftAuthTokenError {
///
/// ```
/// # async fn example(client: &reqwest::Client) -> Result<(), Box<dyn std::error::Error>> {
/// 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(())
/// # }
/// ```
pub async fn get_ms_link_code(
client: &reqwest::Client,
client_id: Option<&str>,
scope: Option<&str>,
) -> Result<DeviceCodeResponse, GetMicrosoftAuthTokenError> {
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()
Expand All @@ -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<ExpiringValue<AccessTokenResponse>, 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 {
Expand All @@ -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"),
])
Expand Down Expand Up @@ -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<ExpiringValue<AccessTokenResponse>, 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)]
Expand All @@ -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<ExpiringValue<AccessTokenResponse>, 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),
])
Expand Down
38 changes: 35 additions & 3 deletions azalea-client/src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, azalea_auth::AuthError> {
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<Self, azalea_auth::AuthError> {
let minecraft_dir = minecraft_folder_path::minecraft_dir().unwrap_or_else(|| {
panic!(
"No {} environment variable found",
Expand All @@ -100,6 +112,8 @@ impl Account {
email,
azalea_auth::AuthOpts {
cache_file: Some(minecraft_dir.join("azalea-auth.json")),
client_id,
scope,
..Default::default()
},
)
Expand Down Expand Up @@ -128,24 +142,42 @@ impl Account {
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// 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<AccessTokenResponse>,
) -> Result<Self, azalea_auth::AuthError> {
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<AccessTokenResponse>,
client_id: Option<&str>,
scope: Option<&str>,
) -> Result<Self, azalea_auth::AuthError> {
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;
Expand Down

0 comments on commit 13afc1d

Please sign in to comment.