Auth Customization Options (#159)

* 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 <27899617+mat-1@users.noreply.github.com>

* 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 <27899617+mat-1@users.noreply.github.com>
This commit is contained in:
Aditya Kumar 2024-08-12 03:24:45 +05:30 committed by GitHub
parent 92c90753ea
commit 13afc1d6a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 96 additions and 20 deletions

View file

@ -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?)
}

View file

@ -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<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)]
@ -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 {
@ -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?;
}
}
}
@ -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<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(())
@ -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<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()
@ -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 {
@ -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<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)]
@ -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),
])

View file

@ -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",
@ -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<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;