diff options
| author | Adam Reisenauer <58893124+Mythbusters123@users.noreply.github.com> | 2023-06-24 18:09:43 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-06-24 17:09:43 -0500 |
| commit | 5e4699688207b8ac722ae7f96c49428242f49a9d (patch) | |
| tree | 52b0f9b8507fa380376dab81411cb74eed413c95 /azalea-auth/src | |
| parent | fe687f9bdbdf3e0214ac4ac6da47a181e4dc23dd (diff) | |
| download | azalea-drasl-5e4699688207b8ac722ae7f96c49428242f49a9d.tar.xz | |
Add functions `auth_with_link_code`, `get_ms_link_code`, and `get_ms_auth_token`. (#88)
* Add option for grabbing authentication code for Microsoft seperately. Created two new functions, one that outputs the DeviceCodeResponse and one that uses this response to authenticate an actual account.
* Added documentation and cleaned up function names. Still wondering about code repeition
* reduce code duplication, more docs, cleanup
* clippy
---------
Co-authored-by: mat <git@matdoes.dev>
Diffstat (limited to 'azalea-auth/src')
| -rwxr-xr-x | azalea-auth/src/auth.rs | 220 | ||||
| -rwxr-xr-x | azalea-auth/src/cache.rs | 9 | ||||
| -rwxr-xr-x | azalea-auth/src/lib.rs | 2 |
3 files changed, 156 insertions, 75 deletions
diff --git a/azalea-auth/src/auth.rs b/azalea-auth/src/auth.rs index bb72c8c4..b0b30e1f 100755 --- a/azalea-auth/src/auth.rs +++ b/azalea-auth/src/auth.rs @@ -55,6 +55,10 @@ pub enum AuthError { /// The email is technically only used as a cache key, so it *could* be /// anything. You should just have it be the actual email so it's not confusing /// though, and in case the Microsoft API does start providing the real email. +/// +/// 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 instead. pub async fn auth(email: &str, opts: AuthOpts) -> Result<AuthResult, AuthError> { let cached_account = if let Some(cache_file) = &opts.cache_file { cache::get_account_in_cache(cache_file, email).await @@ -62,16 +66,15 @@ pub async fn auth(email: &str, opts: AuthOpts) -> Result<AuthResult, AuthError> None }; - // these two MUST be set by the end, since we return them in AuthResult - let profile: ProfileResponse; - let minecraft_access_token: String; - if cached_account.is_some() && !cached_account.as_ref().unwrap().mca.is_expired() { let account = cached_account.as_ref().unwrap(); // the minecraft auth data is cached and not expired, so we can just // use that instead of doing auth all over again :) - profile = account.profile.clone(); - minecraft_access_token = account.mca.data.access_token.clone(); + + Ok(AuthResult { + access_token: account.mca.data.access_token.clone(), + profile: account.profile.clone(), + }) } else { let client = reqwest::Client::new(); let mut msa = if let Some(account) = cached_account { @@ -83,37 +86,20 @@ pub async fn auth(email: &str, opts: AuthOpts) -> Result<AuthResult, AuthError> log::trace!("refreshing Microsoft auth token"); msa = refresh_ms_auth_token(&client, &msa.data.refresh_token).await?; } - let ms_access_token = &msa.data.access_token; - log::trace!("Got access token: {}", ms_access_token); - - let xbl_auth = auth_with_xbox_live(&client, ms_access_token).await?; - - let xsts_token = obtain_xsts_for_minecraft( - &client, - &xbl_auth - .get() - .expect("Xbox Live auth token shouldn't have expired yet") - .token, - ) - .await?; - // Minecraft auth - let mca = auth_with_minecraft(&client, &xbl_auth.data.user_hash, &xsts_token).await?; + let msa_token = &msa.data.access_token; + log::trace!("Got access token: {msa_token}"); - minecraft_access_token = mca - .get() - .expect("Minecraft auth shouldn't have expired yet") - .access_token - .to_string(); + let res = get_minecraft_token(&client, msa_token).await?; if opts.check_ownership { - let has_game = check_ownership(&client, &minecraft_access_token).await?; + let has_game = check_ownership(&client, &res.minecraft_access_token).await?; if !has_game { return Err(AuthError::DoesNotOwnGame); } } - profile = get_profile(&client, &minecraft_access_token).await?; + let profile: ProfileResponse = get_profile(&client, &res.minecraft_access_token).await?; if let Some(cache_file) = opts.cache_file { if let Err(e) = cache::set_account_in_cache( @@ -121,9 +107,9 @@ pub async fn auth(email: &str, opts: AuthOpts) -> Result<AuthResult, AuthError> email, CachedAccount { email: email.to_string(), - mca, + mca: res.mca, msa, - xbl: xbl_auth, + xbl: res.xbl, profile: profile.clone(), }, ) @@ -132,15 +118,60 @@ pub async fn auth(email: &str, opts: AuthOpts) -> Result<AuthResult, AuthError> log::error!("{}", e); } } + + Ok(AuthResult { + access_token: res.minecraft_access_token, + profile, + }) } +} + +/// Authenticate with Minecraft when we already have a Microsoft auth token. +/// +/// Usually you don't need this since [`auth`] will call it for you, but it's +/// useful if you want more control over what it does. +/// +/// If you don't have a Microsoft auth token, you can get it from +/// [`get_ms_link_code`] and then [`get_ms_auth_token`]. +pub async fn get_minecraft_token( + client: &reqwest::Client, + msa: &str, +) -> Result<MinecraftTokenResponse, AuthError> { + let xbl_auth = auth_with_xbox_live(client, msa).await?; - Ok(AuthResult { - access_token: minecraft_access_token, - profile, + let xsts_token = obtain_xsts_for_minecraft( + client, + &xbl_auth + .get() + .expect("Xbox Live auth token shouldn't have expired yet") + .token, + ) + .await?; + + // Minecraft auth + let mca = auth_with_minecraft(client, &xbl_auth.data.user_hash, &xsts_token).await?; + + let minecraft_access_token: String = mca + .get() + .expect("Minecraft auth shouldn't have expired yet") + .access_token + .to_string(); + + Ok(MinecraftTokenResponse { + mca, + xbl: xbl_auth, + minecraft_access_token, }) } #[derive(Debug)] +pub struct MinecraftTokenResponse { + pub mca: ExpiringValue<MinecraftAuthResponse>, + pub xbl: ExpiringValue<XboxLiveAuth>, + pub minecraft_access_token: String, +} + +#[derive(Debug)] pub struct AuthResult { pub access_token: String, pub profile: ProfileResponse, @@ -148,64 +179,63 @@ pub struct AuthResult { #[derive(Debug, Deserialize)] pub struct DeviceCodeResponse { - user_code: String, - device_code: String, - verification_uri: String, - expires_in: u64, - interval: u64, + pub user_code: String, + pub device_code: String, + pub verification_uri: String, + pub expires_in: u64, + pub interval: u64, } #[allow(unused)] -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct AccessTokenResponse { - token_type: String, - expires_in: u64, - scope: String, - access_token: String, - refresh_token: String, - user_id: String, + pub token_type: String, + pub expires_in: u64, + pub scope: String, + pub access_token: String, + pub refresh_token: String, + pub user_id: String, } #[allow(unused)] #[derive(Debug, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct XboxLiveAuthResponse { - issue_instant: String, - not_after: String, - token: String, - display_claims: HashMap<String, Vec<HashMap<String, String>>>, + pub issue_instant: String, + pub not_after: String, + pub token: String, + pub display_claims: HashMap<String, Vec<HashMap<String, String>>>, } /// Just the important data #[derive(Serialize, Deserialize, Debug)] pub struct XboxLiveAuth { - token: String, - user_hash: String, + pub token: String, + pub user_hash: String, } #[allow(unused)] #[derive(Debug, Deserialize, Serialize)] pub struct MinecraftAuthResponse { - username: String, - roles: Vec<String>, - access_token: String, - token_type: String, - expires_in: u64, + pub username: String, + pub roles: Vec<String>, + pub access_token: String, + pub token_type: String, + pub expires_in: u64, } -#[allow(unused)] #[derive(Debug, Deserialize)] pub struct GameOwnershipResponse { - items: Vec<GameOwnershipItem>, - signature: String, - key_id: String, + pub items: Vec<GameOwnershipItem>, + pub signature: String, + pub key_id: String, } #[allow(unused)] #[derive(Debug, Deserialize)] pub struct GameOwnershipItem { - name: String, - signature: String, + pub name: String, + pub signature: String, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -227,12 +257,33 @@ pub enum GetMicrosoftAuthTokenError { Timeout, } -/// Asks the user to go to a webpage and log in with Microsoft. -async fn interactive_get_ms_auth_token( +/// Get the Microsoft link code that's shown to the user for logging into +/// Microsoft. +/// +/// You should call [`get_ms_auth_token`] right after showing the user the +/// [`verification_uri`](DeviceCodeResponse::verification_uri) and +/// [`user_code`](DeviceCodeResponse::user_code). +/// +/// If showing the link code in the terminal is acceptable, then you can just +/// use [`interactive_get_ms_auth_token`] instead. +/// +/// ``` +/// # async fn example(client: &reqwest::Client) -> Result<(), Box<dyn std::error::Error>> { +/// let res = azalea_auth::get_ms_link_code(&client).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 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, - email: &str, -) -> Result<ExpiringValue<AccessTokenResponse>, GetMicrosoftAuthTokenError> { - let res = client +) -> Result<DeviceCodeResponse, GetMicrosoftAuthTokenError> { + Ok(client .post("https://login.live.com/oauth20_connect.srf") .form(&vec![ ("scope", "service::user.auth.xboxlive.com::MBI_SSL"), @@ -242,13 +293,17 @@ async fn interactive_get_ms_auth_token( .send() .await? .json::<DeviceCodeResponse>() - .await?; - log::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 - ); + .await?) +} +/// Wait until the user logged into Microsoft with the given code. You get the +/// device code response needed for this function from [`get_ms_link_code`]. +/// +/// You should pass the response from this to [`get_minecraft_token`]. +pub async fn get_ms_auth_token( + client: &reqwest::Client, + res: DeviceCodeResponse, +) -> Result<ExpiringValue<AccessTokenResponse>, GetMicrosoftAuthTokenError> { let login_expires_at = Instant::now() + std::time::Duration::from_secs(res.expires_in); while Instant::now() < login_expires_at { @@ -285,13 +340,30 @@ async fn interactive_get_ms_auth_token( Err(GetMicrosoftAuthTokenError::Timeout) } +/// Asks the user to go to a webpage and log in with Microsoft. If you need to +/// access the code, then use [`get_ms_link_code`] and then +/// [`get_ms_auth_token`] instead. +pub async fn interactive_get_ms_auth_token( + client: &reqwest::Client, + email: &str, +) -> Result<ExpiringValue<AccessTokenResponse>, GetMicrosoftAuthTokenError> { + let res = get_ms_link_code(client).await?; + log::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 +} + #[derive(Debug, Error)] pub enum RefreshMicrosoftAuthTokenError { #[error("Http error: {0}")] Http(#[from] reqwest::Error), } -async fn refresh_ms_auth_token( +pub async fn refresh_ms_auth_token( client: &reqwest::Client, refresh_token: &str, ) -> Result<ExpiringValue<AccessTokenResponse>, RefreshMicrosoftAuthTokenError> { diff --git a/azalea-auth/src/cache.rs b/azalea-auth/src/cache.rs index 404b05ff..1e8aee10 100755 --- a/azalea-auth/src/cache.rs +++ b/azalea-auth/src/cache.rs @@ -58,6 +58,15 @@ impl<T> ExpiringValue<T> { } } +impl<T: Clone> Clone for ExpiringValue<T> { + fn clone(&self) -> Self { + Self { + expires_at: self.expires_at, + data: self.data.clone(), + } + } +} + async fn get_entire_cache(cache_file: &Path) -> Result<Vec<CachedAccount>, CacheError> { let mut cache: Vec<CachedAccount> = Vec::new(); if cache_file.exists() { diff --git a/azalea-auth/src/lib.rs b/azalea-auth/src/lib.rs index cf0d0401..bd151eb3 100755 --- a/azalea-auth/src/lib.rs +++ b/azalea-auth/src/lib.rs @@ -1,7 +1,7 @@ #![doc = include_str!("../README.md")] mod auth; -mod cache; +pub mod cache; pub mod certs; pub mod game_profile; pub mod sessionserver; |
