Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth Customization Options #159

Merged
merged 18 commits into from
Aug 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading