From 4cef62e8e4aa04e44048eb67e5091c12a73d2a09 Mon Sep 17 00:00:00 2001 From: mat <27899617+mat-1@users.noreply.github.com> Date: Sun, 16 Oct 2022 22:54:54 -0500 Subject: Microsoft Authentication (#29) * a * try to do more work on auth signing (untested) * well auth works when i remove the d= so * auth stuff * sessionserver stuff * add auth in azalea-protocol/client * caching* refreshing microsoft auth tokens isn't implemented yet, also i haven't tested it * how did i not notice that i had the code duplicated * fix cache * add refreshing msa token * replace some printlns with log::trace * auth works! * Update main.rs * fix clippy warnings --- azalea-auth/src/cache.rs | 105 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 azalea-auth/src/cache.rs (limited to 'azalea-auth/src/cache.rs') diff --git a/azalea-auth/src/cache.rs b/azalea-auth/src/cache.rs new file mode 100644 index 00000000..8af9e171 --- /dev/null +++ b/azalea-auth/src/cache.rs @@ -0,0 +1,105 @@ +//! Cache auth information + +use serde::{Deserialize, Serialize}; +use std::path::Path; +use std::time::{SystemTime, UNIX_EPOCH}; +use thiserror::Error; +use tokio::fs::File; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +#[derive(Debug, Error)] +pub enum CacheError { + #[error("Failed to read cache file: {0}")] + Read(std::io::Error), + #[error("Failed to write cache file: {0}")] + Write(std::io::Error), + #[error("Failed to parse cache file: {0}")] + Parse(serde_json::Error), +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct CachedAccount { + pub email: String, + /// Microsoft auth + pub msa: ExpiringValue, + /// Xbox Live auth + pub xbl: ExpiringValue, + /// Minecraft auth + pub mca: ExpiringValue, + /// The user's Minecraft profile (i.e. username, UUID, skin) + pub profile: crate::auth::ProfileResponse, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct ExpiringValue { + /// Seconds since the UNIX epoch + pub expires_at: u64, + pub data: T, +} + +impl ExpiringValue { + pub fn is_expired(&self) -> bool { + self.expires_at + < SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + } + + /// Return the data if it's not expired, otherwise return `None` + pub fn get(&self) -> Option<&T> { + if self.is_expired() { + None + } else { + Some(&self.data) + } + } +} + +async fn get_entire_cache(cache_file: &Path) -> Result, CacheError> { + let mut cache: Vec = Vec::new(); + if cache_file.exists() { + let mut cache_file = File::open(cache_file).await.map_err(CacheError::Read)?; + // read the file into a string + let mut contents = String::new(); + cache_file + .read_to_string(&mut contents) + .await + .map_err(CacheError::Read)?; + cache = serde_json::from_str(&contents).map_err(CacheError::Parse)?; + } + Ok(cache) +} +async fn set_entire_cache(cache_file: &Path, cache: Vec) -> Result<(), CacheError> { + println!("saving cache: {:?}", cache); + + let mut cache_file = File::create(cache_file).await.map_err(CacheError::Write)?; + let cache = serde_json::to_string_pretty(&cache).map_err(CacheError::Parse)?; + cache_file + .write_all(cache.as_bytes()) + .await + .map_err(CacheError::Write)?; + + Ok(()) +} + +/// Gets cached data for the given email. +/// +/// Technically it doesn't actually have to be an email since it's only the +/// cache key. I considered using usernames or UUIDs as the cache key, but +/// usernames change and no one has their UUID memorized. +pub async fn get_account_in_cache(cache_file: &Path, email: &str) -> Option { + let cache = get_entire_cache(cache_file).await.unwrap_or_default(); + cache.into_iter().find(|account| account.email == email) +} + +pub async fn set_account_in_cache( + cache_file: &Path, + email: &str, + account: CachedAccount, +) -> Result<(), CacheError> { + let mut cache = get_entire_cache(cache_file).await.unwrap_or_default(); + cache.retain(|account| account.email != email); + cache.push(account); + set_entire_cache(cache_file, cache).await +} -- cgit v1.2.3