diff --git a/azalea-auth/examples/auth.rs b/azalea-auth/examples/auth.rs index f9e6ad79..bde89814 100644 --- a/azalea-auth/examples/auth.rs +++ b/azalea-auth/examples/auth.rs @@ -1,6 +1,6 @@ #[tokio::main] async fn main() { - let auth_result = azalea_auth::auth(azalea_auth::AuthOpts::default()) + let auth_result = azalea_auth::auth("example@example.com", azalea_auth::AuthOpts::default()) .await .unwrap(); println!("{:?}", auth_result); diff --git a/azalea-auth/src/auth.rs b/azalea-auth/src/auth.rs index f224bcc9..5cb2a781 100644 --- a/azalea-auth/src/auth.rs +++ b/azalea-auth/src/auth.rs @@ -31,9 +31,7 @@ pub struct AuthOpts { /// 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. -pub async fn auth(email: &str, opts: Option) -> anyhow::Result { - let opts = opts.unwrap_or_default(); - +pub async fn auth(email: &str, opts: AuthOpts) -> anyhow::Result { let cached_account = if let Some(cache_file) = &opts.cache_file && let Some(account) = cache::get_account_in_cache(&cache_file, email).await { Some(account) } else { None }; diff --git a/azalea-auth/src/lib.rs b/azalea-auth/src/lib.rs index c55810ef..03e15c71 100755 --- a/azalea-auth/src/lib.rs +++ b/azalea-auth/src/lib.rs @@ -1,316 +1,8 @@ #![feature(let_chains)] -pub mod auth; +mod auth; mod cache; pub mod game_profile; pub mod sessionserver; -use anyhow::anyhow; -use serde::Deserialize; -use serde_json::json; -use std::{collections::HashMap, time::Instant}; - -/// Ask the user to authenticate with Microsoft and return the user's Minecraft -/// access token. There's caching, so if the user has already authenticated -/// before then they won't have to log in manually again. -pub async fn auth(opts: AuthOpts) -> anyhow::Result { - let client = reqwest::Client::new(); - - let auth_token_res = interactive_get_ms_auth_token(&client).await?; - // TODO: cache this - println!("Got access token: {}", auth_token_res.access_token); - - let xbl_auth = auth_with_xbox_live(&client, &auth_token_res.access_token).await?; - - let xsts_token = obtain_xsts_for_minecraft(&client, &xbl_auth).await?; - - let minecraft_access_token = - auth_with_minecraft(&client, &xbl_auth.user_hash, &xsts_token).await?; - - if opts.check_ownership { - let has_game = check_ownership(&client, &minecraft_access_token).await?; - if !has_game { - panic!( - "The Minecraft API is indicating that you don't own the game. \ - If you're using Xbox Game Pass, set `check_ownership` to false in the auth options." - ); - } - } - - // let profile = get_profile(&client, &minecraft_access_token).await?; - - Ok(minecraft_access_token) -} - -pub struct AuthOpts { - /// 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. - pub check_ownership: bool, - // /// Whether we should get the Minecraft profile data (i.e. username, uuid, - // /// skin, etc) for the player. - // pub get_profile: bool, - /// The file to store cached data in. If this is `None`, then no - /// caching will be done. - pub cache_dir: Option, -} - -impl Default for AuthOpts { - fn default() -> Self { - Self { - check_ownership: true, - // get_profile: true, - cache_dir: None, - } - } -} - -#[derive(Debug, Deserialize)] -pub struct DeviceCodeResponse { - user_code: String, - device_code: String, - verification_uri: String, - expires_in: u64, - interval: u64, -} - -#[allow(unused)] -#[derive(Debug, Deserialize)] -pub struct AccessTokenResponse { - token_type: String, - expires_in: u64, - scope: String, - access_token: String, - refresh_token: String, - 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>>, -} - -/// Just the important data -pub struct XboxLiveAuth { - token: String, - user_hash: String, -} - -#[allow(unused)] -#[derive(Debug, Deserialize)] -pub struct MinecraftAuthResponse { - username: String, - roles: Vec, - access_token: String, - token_type: String, - expires_in: u64, -} - -#[allow(unused)] -#[derive(Debug, Deserialize)] -pub struct GameOwnershipResponse { - items: Vec, - signature: String, - key_id: String, -} - -#[allow(unused)] -#[derive(Debug, Deserialize)] -pub struct GameOwnershipItem { - name: String, - signature: String, -} - -#[derive(Debug, Deserialize)] -pub struct ProfileResponse { - pub id: String, - pub name: String, - pub skins: Vec, - pub capes: Vec, -} - -/// Asks the user to go to a webpage and log in with Microsoft. -async fn interactive_get_ms_auth_token( - client: &reqwest::Client, -) -> anyhow::Result { - // nintendo switch (real) - let client_id = "00000000441cc96b"; - - let res = client - .post("https://login.live.com/oauth20_connect.srf") - .form(&vec![ - ("scope", "service::user.auth.xboxlive.com::MBI_SSL"), - ("client_id", client_id), - ("response_type", "device_code"), - ]) - .send() - .await? - .json::() - .await?; - println!("{:?}", res); - println!( - "Go to {} and enter the code {}", - res.verification_uri, res.user_code - ); - - let access_token_response: AccessTokenResponse; - - let expire_time = Instant::now() + std::time::Duration::from_secs(res.expires_in); - - while Instant::now() < expire_time { - tokio::time::sleep(std::time::Duration::from_secs(res.interval)).await; - - println!("trying"); - if let Ok(res) = client - .post(format!( - "https://login.live.com/oauth20_token.srf?client_id={}", - client_id - )) - .form(&vec![ - ("client_id", client_id), - ("device_code", &res.device_code), - ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"), - ]) - .send() - .await? - .json::() - .await - { - access_token_response = res; - return Ok(access_token_response); - } - } - - Err(anyhow!("Authentication timed out")) -} - -async fn auth_with_xbox_live( - client: &reqwest::Client, - access_token: &str, -) -> anyhow::Result { - let auth_json = json!({ - "Properties": { - "AuthMethod": "RPS", - "SiteName": "user.auth.xboxlive.com", - // i thought this was supposed to be d={} but it doesn't work for - // me when i add it ?????? - "RpsTicket": format!("{}", access_token) - }, - "RelyingParty": "http://auth.xboxlive.com", - "TokenType": "JWT" - }); - let payload = auth_json.to_string(); - // let signature = sign( - // "https://user.auth.xboxlive.com/user/authenticate", - // "", - // &payload, - // )?; - println!("auth_json: {:#?}", auth_json); - let res = client - .post("https://user.auth.xboxlive.com/user/authenticate") - .header("Content-Type", "application/json") - .header("Accept", "application/json") - .header("x-xbl-contract-version", "1") - // .header("Cache-Control", "no-store, must-revalidate, no-cache") - // .header("Signature", base64::encode(signature)) - .body(payload) - .send() - .await? - .json::() - .await?; - println!("got res: {:?}", res); - - Ok(XboxLiveAuth { - token: res.token, - user_hash: res.display_claims["xui"].get(0).unwrap()["uhs"].clone(), - }) -} - -async fn obtain_xsts_for_minecraft( - client: &reqwest::Client, - xbl_auth: &XboxLiveAuth, -) -> anyhow::Result { - let res = client - .post("https://xsts.auth.xboxlive.com/xsts/authorize") - .header("Accept", "application/json") - .json(&json!({ - "Properties": { - "SandboxId": "RETAIL", - "UserTokens": [xbl_auth.token] - }, - "RelyingParty": "rp://api.minecraftservices.com/", - "TokenType": "JWT" - })) - .send() - .await? - .json::() - .await?; - println!("{:?}", res); - - Ok(res.token) -} - -async fn auth_with_minecraft( - client: &reqwest::Client, - user_hash: &str, - xsts_token: &str, -) -> anyhow::Result { - let res = client - .post("https://api.minecraftservices.com/authentication/login_with_xbox") - .header("Accept", "application/json") - .json(&json!({ - "identityToken": format!("XBL3.0 x={};{}", user_hash, xsts_token) - })) - .send() - .await? - .json::() - .await?; - println!("{:?}", res); - - Ok(res.access_token) -} - -pub async fn check_ownership( - client: &reqwest::Client, - minecraft_access_token: &str, -) -> anyhow::Result { - let res = client - .get("https://api.minecraftservices.com/entitlements/mcstore") - .header( - "Authorization", - format!("Bearer {}", minecraft_access_token), - ) - .send() - .await? - .json::() - .await?; - println!("{:?}", res); - - // TODO: we *should* check with mojang's public key that the signatures are right - - Ok(!res.items.is_empty()) -} - -pub async fn get_profile( - client: &reqwest::Client, - minecraft_access_token: &str, -) -> anyhow::Result { - let res = client - .get("https://api.minecraftservices.com/minecraft/profile") - .header( - "Authorization", - format!("Bearer {}", minecraft_access_token), - ) - .send() - .await? - .json::() - .await?; - println!("{:?}", res); - - Ok(res) -} +pub use auth::*;