mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 14:26:04 +00:00
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:
parent
92c90753ea
commit
13afc1d6a4
3 changed files with 96 additions and 20 deletions
|
@ -18,15 +18,16 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We will be using default `client_id` and `scope`
|
||||||
async fn auth() -> Result<ProfileResponse, Box<dyn Error>> {
|
async fn auth() -> Result<ProfileResponse, Box<dyn Error>> {
|
||||||
let client = reqwest::Client::new();
|
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!(
|
println!(
|
||||||
"Go to {} and enter the code {}",
|
"Go to {} and enter the code {}",
|
||||||
res.verification_uri, res.user_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?;
|
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?)
|
Ok(azalea_auth::get_profile(&client, &auth_result.minecraft_access_token).await?)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ use thiserror::Error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct AuthOpts {
|
pub struct AuthOpts<'a> {
|
||||||
/// Whether we should check if the user actually owns the game. This will
|
/// 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
|
/// fail if the user has Xbox Game Pass! Note that this isn't really
|
||||||
/// necessary, since getting the user profile will check this anyways.
|
/// 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
|
/// The directory to store the cache in. If this is not set, caching is not
|
||||||
/// done.
|
/// done.
|
||||||
pub cache_file: Option<PathBuf>,
|
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)]
|
#[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
|
/// 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`],
|
/// in a different way, use [`get_ms_link_code`], [`get_ms_auth_token`],
|
||||||
/// [`get_minecraft_token`] and [`get_profile`] instead.
|
/// [`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 {
|
let cached_account = if let Some(cache_file) = &opts.cache_file {
|
||||||
cache::get_account_in_cache(cache_file, email).await
|
cache::get_account_in_cache(cache_file, email).await
|
||||||
} else {
|
} else {
|
||||||
|
@ -76,20 +82,32 @@ pub async fn auth(email: &str, opts: AuthOpts) -> Result<AuthResult, AuthError>
|
||||||
profile: account.profile.clone(),
|
profile: account.profile.clone(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
let client_id = opts.client_id.unwrap_or(CLIENT_ID);
|
||||||
|
let scope = opts.scope.unwrap_or(SCOPE);
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
let mut msa = if let Some(account) = cached_account {
|
let mut msa = if let Some(account) = cached_account {
|
||||||
account.msa
|
account.msa
|
||||||
} else {
|
} 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() {
|
if msa.is_expired() {
|
||||||
tracing::trace!("refreshing Microsoft auth token");
|
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,
|
Ok(new_msa) => msa = new_msa,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// can't refresh, ask the user to auth again
|
// can't refresh, ask the user to auth again
|
||||||
tracing::error!("Error refreshing Microsoft auth token: {}", e);
|
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)
|
// nintendo switch (so it works for accounts that are under 18 years old)
|
||||||
const CLIENT_ID: &str = "00000000441cc96b";
|
const CLIENT_ID: &str = "00000000441cc96b";
|
||||||
|
const SCOPE: &str = "service::user.auth.xboxlive.com::MBI_SSL";
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum GetMicrosoftAuthTokenError {
|
pub enum GetMicrosoftAuthTokenError {
|
||||||
|
@ -280,12 +299,12 @@ pub enum GetMicrosoftAuthTokenError {
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # async fn example(client: &reqwest::Client) -> Result<(), Box<dyn std::error::Error>> {
|
/// # 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!(
|
/// println!(
|
||||||
/// "Go to {} and enter the code {}",
|
/// "Go to {} and enter the code {}",
|
||||||
/// res.verification_uri, res.user_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 minecraft = azalea_auth::get_minecraft_token(client, &msa.data.access_token).await?;
|
||||||
/// let profile = azalea_auth::get_profile(&client, &minecraft.minecraft_access_token).await?;
|
/// let profile = azalea_auth::get_profile(&client, &minecraft.minecraft_access_token).await?;
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
|
@ -293,12 +312,22 @@ pub enum GetMicrosoftAuthTokenError {
|
||||||
/// ```
|
/// ```
|
||||||
pub async fn get_ms_link_code(
|
pub async fn get_ms_link_code(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
|
client_id: Option<&str>,
|
||||||
|
scope: Option<&str>,
|
||||||
) -> Result<DeviceCodeResponse, GetMicrosoftAuthTokenError> {
|
) -> 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
|
Ok(client
|
||||||
.post("https://login.live.com/oauth20_connect.srf")
|
.post("https://login.live.com/oauth20_connect.srf")
|
||||||
.form(&vec![
|
.form(&vec![
|
||||||
("scope", "service::user.auth.xboxlive.com::MBI_SSL"),
|
("scope", scope),
|
||||||
("client_id", CLIENT_ID),
|
("client_id", client_id),
|
||||||
("response_type", "device_code"),
|
("response_type", "device_code"),
|
||||||
])
|
])
|
||||||
.send()
|
.send()
|
||||||
|
@ -314,7 +343,14 @@ pub async fn get_ms_link_code(
|
||||||
pub async fn get_ms_auth_token(
|
pub async fn get_ms_auth_token(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
res: DeviceCodeResponse,
|
res: DeviceCodeResponse,
|
||||||
|
client_id: Option<&str>,
|
||||||
) -> Result<ExpiringValue<AccessTokenResponse>, GetMicrosoftAuthTokenError> {
|
) -> 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);
|
let login_expires_at = Instant::now() + std::time::Duration::from_secs(res.expires_in);
|
||||||
|
|
||||||
while Instant::now() < login_expires_at {
|
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...");
|
tracing::trace!("Polling to check if user has logged in...");
|
||||||
if let Ok(access_token_response) = client
|
if let Ok(access_token_response) = client
|
||||||
.post(format!(
|
.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![
|
.form(&vec![
|
||||||
("client_id", CLIENT_ID),
|
("client_id", client_id),
|
||||||
("device_code", &res.device_code),
|
("device_code", &res.device_code),
|
||||||
("grant_type", "urn:ietf:params:oauth:grant-type: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(
|
pub async fn interactive_get_ms_auth_token(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
email: &str,
|
email: &str,
|
||||||
|
client_id: Option<&str>,
|
||||||
|
scope: Option<&str>,
|
||||||
) -> Result<ExpiringValue<AccessTokenResponse>, GetMicrosoftAuthTokenError> {
|
) -> 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);
|
tracing::trace!("Device code response: {:?}", res);
|
||||||
println!(
|
println!(
|
||||||
"Go to \x1b[1m{}\x1b[m and enter the code \x1b[1m{}\x1b[m for \x1b[1m{}\x1b[m",
|
"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
|
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)]
|
#[derive(Debug, Error)]
|
||||||
|
@ -379,12 +417,17 @@ pub enum RefreshMicrosoftAuthTokenError {
|
||||||
pub async fn refresh_ms_auth_token(
|
pub async fn refresh_ms_auth_token(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
refresh_token: &str,
|
refresh_token: &str,
|
||||||
|
client_id: Option<&str>,
|
||||||
|
scope: Option<&str>,
|
||||||
) -> Result<ExpiringValue<AccessTokenResponse>, RefreshMicrosoftAuthTokenError> {
|
) -> 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
|
let access_token_response_text = client
|
||||||
.post("https://login.live.com/oauth20_token.srf")
|
.post("https://login.live.com/oauth20_token.srf")
|
||||||
.form(&vec![
|
.form(&vec![
|
||||||
("scope", "service::user.auth.xboxlive.com::MBI_SSL"),
|
("scope", scope),
|
||||||
("client_id", CLIENT_ID),
|
("client_id", client_id),
|
||||||
("grant_type", "refresh_token"),
|
("grant_type", "refresh_token"),
|
||||||
("refresh_token", refresh_token),
|
("refresh_token", refresh_token),
|
||||||
])
|
])
|
||||||
|
|
|
@ -90,6 +90,18 @@ impl Account {
|
||||||
/// a key for the cache, but it's recommended to use the real email to
|
/// a key for the cache, but it's recommended to use the real email to
|
||||||
/// avoid confusion.
|
/// avoid confusion.
|
||||||
pub async fn microsoft(email: &str) -> Result<Self, azalea_auth::AuthError> {
|
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(|| {
|
let minecraft_dir = minecraft_folder_path::minecraft_dir().unwrap_or_else(|| {
|
||||||
panic!(
|
panic!(
|
||||||
"No {} environment variable found",
|
"No {} environment variable found",
|
||||||
|
@ -100,6 +112,8 @@ impl Account {
|
||||||
email,
|
email,
|
||||||
azalea_auth::AuthOpts {
|
azalea_auth::AuthOpts {
|
||||||
cache_file: Some(minecraft_dir.join("azalea-auth.json")),
|
cache_file: Some(minecraft_dir.join("azalea-auth.json")),
|
||||||
|
client_id,
|
||||||
|
scope,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -128,24 +142,42 @@ impl Account {
|
||||||
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let client = reqwest::Client::new();
|
/// 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!(
|
/// println!(
|
||||||
/// "Go to {} and enter the code {}",
|
/// "Go to {} and enter the code {}",
|
||||||
/// res.verification_uri, res.user_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?;
|
/// Account::with_microsoft_access_token(msa).await?;
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
pub async fn with_microsoft_access_token(
|
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>,
|
mut msa: azalea_auth::cache::ExpiringValue<AccessTokenResponse>,
|
||||||
|
client_id: Option<&str>,
|
||||||
|
scope: Option<&str>,
|
||||||
) -> Result<Self, azalea_auth::AuthError> {
|
) -> Result<Self, azalea_auth::AuthError> {
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
if msa.is_expired() {
|
if msa.is_expired() {
|
||||||
tracing::trace!("refreshing Microsoft auth token");
|
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;
|
let msa_token = &msa.data.access_token;
|
||||||
|
|
Loading…
Add table
Reference in a new issue