From 19df8b416f57271044e1aaab2acbba96569ec105 Mon Sep 17 00:00:00 2001 From: Lizzy Fleckenstein Date: Sun, 10 May 2026 16:49:01 +0200 Subject: support yggdrasil accounts --- azalea-client/src/account/mod.rs | 2 + azalea-client/src/account/yggdrasil.rs | 196 +++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 azalea-client/src/account/yggdrasil.rs diff --git a/azalea-client/src/account/mod.rs b/azalea-client/src/account/mod.rs index 23ca7976..7b8192c3 100644 --- a/azalea-client/src/account/mod.rs +++ b/azalea-client/src/account/mod.rs @@ -3,6 +3,8 @@ #[cfg(feature = "online-mode")] pub mod microsoft; pub mod offline; +#[cfg(feature = "online-mode")] +pub mod yggdrasil; use std::{fmt::Debug, ops::Deref, pin::Pin, sync::Arc}; diff --git a/azalea-client/src/account/yggdrasil.rs b/azalea-client/src/account/yggdrasil.rs new file mode 100644 index 00000000..9965030d --- /dev/null +++ b/azalea-client/src/account/yggdrasil.rs @@ -0,0 +1,196 @@ +use std::path::PathBuf; + +use azalea_auth::{ + certs::Certificates, + sessionserver::{self, ClientSessionServerError, SessionServerJoinOpts}, + yggdrasil::{YggdrasilAuthError, YggdrasilAuthOpts, yggdrasil_auth}, +}; +use parking_lot::Mutex; +use uuid::Uuid; + +use crate::account::{Account, AccountTrait, BoxFuture}; + +fn default_cache_file() -> PathBuf { + let minecraft_dir = minecraft_folder_path::minecraft_dir().unwrap_or_else(|| { + panic!( + "No {} environment variable found", + minecraft_folder_path::home_env_var() + ) + }); + minecraft_dir.join("azalea-auth-yggdrasil.json") +} + +fn default_auth_opts() -> YggdrasilAuthOpts { + YggdrasilAuthOpts { + cache_file: Some(default_cache_file()), + } +} + +#[derive(Debug, Clone)] +pub struct Backend { + pub auth: String, + pub session: String, + pub player: Option, +} + +impl Backend { + pub fn new_drasl(base_url: &str, certs: bool) -> Self { + Self { + auth: format!("{base_url}/auth"), + session: format!("{base_url}/session"), + player: certs.then(|| format!("{base_url}/player")), + } + } +} + +/// A type of account that authenticates with an Yggdrasil server using Azalea's +/// cache. +/// +/// This type is not intended to be used directly by the user. To actually make +/// an account that authenticates with an Yggdrasil server, see +/// [`Account::yggdrasil`] or [`Account::yggdrasil_with_opts`]. +#[derive(Debug)] +pub struct YggdrasilAccount { + auth_opts: YggdrasilAuthOpts, + backend: Backend, + + username: String, + password: Option, + uuid: Uuid, + + access_token: Mutex, + certs: Mutex>, +} + +impl YggdrasilAccount { + // deliberately private, use `Account::yggdrasil` or + // `Account::yggdrasil_with_opts` instead. + async fn new( + username: String, + password: Option, + backend: Backend, + auth_opts: YggdrasilAuthOpts, + ) -> Result { + let auth_result = yggdrasil_auth( + &username, + password.as_deref(), + &backend.auth, + auth_opts.clone(), + ) + .await?; + + Ok(Self { + username, + password, + uuid: auth_result.user.id, + access_token: Mutex::new(auth_result.access_token), + certs: Mutex::new(None), + backend, + auth_opts, + }) + } +} +impl AccountTrait for YggdrasilAccount { + fn username(&self) -> &str { + &self.username + } + fn uuid(&self) -> Uuid { + self.uuid + } + fn access_token(&self) -> Option { + Some(self.access_token.lock().to_owned()) + } + fn certs(&self) -> Option { + self.certs.lock().as_ref().cloned() + } + fn set_certs(&self, certs: azalea_auth::certs::Certificates) { + *self.certs.lock() = Some(certs); + } + fn certs_backend(&self) -> Option<&str> { + self.backend.player.as_deref() + } + fn refresh(&self) -> BoxFuture<'_, Result<(), azalea_auth::AuthError>> { + Box::pin(async { + let new_account = YggdrasilAccount::new( + self.username.clone(), + self.password.clone(), + self.backend.clone(), + self.auth_opts.clone(), + ) + .await?; + let new_access_token = new_account.access_token().unwrap(); + *self.access_token.lock() = new_access_token; + Ok(()) + }) + } + fn join<'a>( + &'a self, + public_key: &'a [u8], + private_key: &'a [u8; 16], + server_id: &'a str, + proxy: Option, + ) -> BoxFuture<'a, Result<(), ClientSessionServerError>> { + Box::pin(async move { + let access_token = self.access_token.lock().clone(); + sessionserver::join_with_backend_url( + SessionServerJoinOpts { + access_token: &access_token, + public_key, + private_key, + uuid: &self.uuid(), + server_id, + proxy, + }, + &self.backend.session, + ) + .await + }) + } +} + +impl Account { + /// This will create an online-mode account by authenticating with + /// an Yggdrasil server. + /// + /// The cache key is used for avoiding having to log in every time. This is + /// typically set to the account email, but it can be any string. + #[cfg(feature = "online-mode")] + pub async fn yggdrasil(username: String, backend: Backend) -> Result { + YggdrasilAccount::new(username, None, backend, default_auth_opts()) + .await + .map(Account::from) + } + + #[cfg(feature = "online-mode")] + pub async fn yggdrasil_with_password( + username: String, + password: String, + backend: Backend, + ) -> Result { + YggdrasilAccount::new(username, Some(password), backend, default_auth_opts()) + .await + .map(Account::from) + } + + /// Similar to [`Account::yggdrasil`] but you can pass custom auth options + /// (including the cache file location). + /// + /// For a custom cache directory, set + /// `auth_opts.cache_file = + /// Some(custom_dir.join("azalea-auth-yggdrasil.json"))`. + /// + /// If `auth_opts.cache_file` is `None`, it will default to Azalea's + /// standard cache file (`~/.minecraft/azalea-auth-yggdrasil.json`) to match + /// [`Account::yggdrasil`]. + #[cfg(feature = "online-mode")] + pub async fn yggdrasil_with_opts( + username: String, + password: Option, + backend: Backend, + auth_opts: YggdrasilAuthOpts, + ) -> Result { + YggdrasilAccount::new(username, password, backend, auth_opts) + .await + .map(Account::from) + } +} -- cgit v1.2.3