aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLizzy Fleckenstein <lizzy@vlhl.dev>2026-05-10 16:49:01 +0200
committerLizzy Fleckenstein <lizzy@vlhl.dev>2026-05-10 16:49:37 +0200
commit19df8b416f57271044e1aaab2acbba96569ec105 (patch)
tree846e58a09006237d2c5643d5f3e1545c9187417a
parent871ef4abac1023d33714d2fda9c54cc63e20a3a5 (diff)
downloadazalea-drasl-main.tar.xz
support yggdrasil accountsHEADmain
-rw-r--r--azalea-client/src/account/mod.rs2
-rw-r--r--azalea-client/src/account/yggdrasil.rs196
2 files changed, 198 insertions, 0 deletions
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<String>,
+}
+
+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<String>,
+ uuid: Uuid,
+
+ access_token: Mutex<String>,
+ certs: Mutex<Option<Certificates>>,
+}
+
+impl YggdrasilAccount {
+ // deliberately private, use `Account::yggdrasil` or
+ // `Account::yggdrasil_with_opts` instead.
+ async fn new(
+ username: String,
+ password: Option<String>,
+ backend: Backend,
+ auth_opts: YggdrasilAuthOpts,
+ ) -> Result<Self, YggdrasilAuthError> {
+ 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<String> {
+ Some(self.access_token.lock().to_owned())
+ }
+ fn certs(&self) -> Option<azalea_auth::certs::Certificates> {
+ 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<reqwest::Proxy>,
+ ) -> 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<Self, YggdrasilAuthError> {
+ 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<Self, YggdrasilAuthError> {
+ 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<String>,
+ backend: Backend,
+ auth_opts: YggdrasilAuthOpts,
+ ) -> Result<Self, YggdrasilAuthError> {
+ YggdrasilAccount::new(username, password, backend, auth_opts)
+ .await
+ .map(Account::from)
+ }
+}