1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 06:16:04 +00:00

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>
This commit is contained in:
Adam Reisenauer 2023-06-24 18:09:43 -04:00 committed by GitHub
parent fe687f9bdb
commit 5e46996882
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 269 additions and 90 deletions

1
Cargo.lock generated
View file

@ -309,6 +309,7 @@ dependencies = [
"once_cell",
"parking_lot",
"regex",
"reqwest",
"thiserror",
"tokio",
"uuid",

View file

@ -0,0 +1,32 @@
//! Authenticate with Microsoft and get a Minecraft profile, but don't cache and
//! use our own code to display the link code.
//!
//! If you still want it to cache, look at the code in [`azalea_auth::auth`] and
//! see how that does it.
use std::error::Error;
use azalea_auth::ProfileResponse;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
env_logger::init();
let profile = auth().await?;
println!("Logged in as {}", profile.name);
Ok(())
}
async fn auth() -> Result<ProfileResponse, Box<dyn Error>> {
let client = reqwest::Client::new();
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 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

@ -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 msa_token = &msa.data.access_token;
log::trace!("Got access token: {msa_token}");
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?;
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,14 +118,59 @@ pub async fn auth(email: &str, opts: AuthOpts) -> Result<AuthResult, AuthError>
log::error!("{}", e);
}
}
}
Ok(AuthResult {
access_token: minecraft_access_token,
profile,
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?;
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,
@ -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> {

View file

@ -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() {

View file

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

View file

@ -9,6 +9,7 @@ version = "0.7.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
reqwest = { version = "0.11.12", default-features = false }
anyhow = "1.0.59"
async-trait = "0.1.58"
azalea-auth = { path = "../azalea-auth", version = "0.7.0" }

View file

@ -4,6 +4,7 @@ use std::sync::Arc;
use crate::get_mc_dir;
use azalea_auth::certs::{Certificates, FetchCertificatesError};
use azalea_auth::AccessTokenResponse;
use parking_lot::Mutex;
use thiserror::Error;
use uuid::Uuid;
@ -55,8 +56,15 @@ pub struct Account {
/// The parameters that were passed for creating the associated [`Account`].
#[derive(Clone, Debug)]
pub enum AccountOpts {
Offline { username: String },
Microsoft { email: String },
Offline {
username: String,
},
Microsoft {
email: String,
},
MicrosoftWithAccessToken {
msa: Arc<Mutex<azalea_auth::cache::ExpiringValue<AccessTokenResponse>>>,
},
}
impl Account {
@ -106,27 +114,82 @@ impl Account {
})
}
/// This will create an online-mode account through
/// [`azalea_auth::get_minecraft_token`] so you can have more control over
/// the authentication process (like doing your own caching or
/// displaying the Microsoft user code to the user in a different way).
///
/// Note that this will not refresh the token when it expires.
///
/// ```
/// # use azalea_client::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?;
/// println!(
/// "Go to {} and enter the code {}",
/// res.verification_uri, res.user_code
/// );
/// let msa = azalea_auth::get_ms_auth_token(&client, res).await?;
/// Account::with_microsoft_access_token(msa).await?;
/// # Ok(())
/// # }
/// ```
pub async fn with_microsoft_access_token(
mut msa: azalea_auth::cache::ExpiringValue<AccessTokenResponse>,
) -> Result<Self, azalea_auth::AuthError> {
let client = reqwest::Client::new();
if msa.is_expired() {
log::trace!("refreshing Microsoft auth token");
msa = azalea_auth::refresh_ms_auth_token(&client, &msa.data.refresh_token).await?;
}
let msa_token = &msa.data.access_token;
let res = azalea_auth::get_minecraft_token(&client, msa_token).await?;
let profile = azalea_auth::get_profile(&client, &res.minecraft_access_token).await?;
Ok(Self {
username: profile.name,
access_token: Some(Arc::new(Mutex::new(res.minecraft_access_token))),
uuid: Some(profile.id),
account_opts: AccountOpts::MicrosoftWithAccessToken {
msa: Arc::new(Mutex::new(msa)),
},
certs: None,
})
}
/// Refresh the access_token for this account to be valid again.
///
/// This requires the `auth_opts` field to be set correctly (which is done
/// by default if you used the constructor functions). Note that if the
/// Account is offline-mode, this function won't do anything.
/// Account is offline-mode then this function won't do anything.
pub async fn refresh(&self) -> Result<(), azalea_auth::AuthError> {
match &self.account_opts {
// offline mode doesn't need to refresh so just don't do anything lol
AccountOpts::Offline { .. } => Ok(()),
AccountOpts::Microsoft { email } => {
let new_account = Account::microsoft(email).await?;
let access_token = self
.access_token
.as_ref()
.expect("Access token should always be set for Microsoft accounts");
let new_access_token = new_account
.access_token
.expect("Access token should always be set for Microsoft accounts")
.lock()
.clone();
*access_token.lock() = new_access_token;
let access_token_mutex = self.access_token.as_ref().unwrap();
let new_access_token = new_account.access_token.unwrap().lock().clone();
*access_token_mutex.lock() = new_access_token;
Ok(())
}
AccountOpts::MicrosoftWithAccessToken { msa } => {
let msa_value = msa.lock().clone();
let new_account = Account::with_microsoft_access_token(msa_value).await?;
let access_token_mutex = self.access_token.as_ref().unwrap();
let new_access_token = new_account.access_token.unwrap().lock().clone();
*access_token_mutex.lock() = new_access_token;
let AccountOpts::MicrosoftWithAccessToken { msa: new_msa } =
new_account.account_opts else { unreachable!() };
*msa.lock() = new_msa.lock().clone();
Ok(())
}
}

View file

@ -79,7 +79,7 @@ pub struct InventoryComponent {
/// The item that is currently held by the cursor. `Slot::Empty` if nothing
/// is currently being held.
///
/// This is different from [`Self::hotbar_selected_index`], which is the
/// This is different from [`Self::selected_hotbar_slot`], which is the
/// item that's selected in the hotbar.
pub carried: ItemSlot,
/// An identifier used by the server to track client inventory desyncs. This

View file

@ -179,6 +179,7 @@ pub fn generate(input: &DeclareMenus) -> TokenStream {
/// Get the range of slot indexes that contain the player's hotbar. This may be different for each menu.
///
/// ```
/// # let inventory = azalea_inventory::Menu::Player(azalea_inventory::Player::default());
/// let hotbar_items = &inventory.slots()[inventory.hotbar_slots_range()];
/// ```
pub fn hotbar_slots_range(&self) -> RangeInclusive<usize> {