Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic Auth Flows #137

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"image": "mcr.microsoft.com/devcontainers/rust",
}
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion azalea-auth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ thiserror = "1.0.57"
tokio = { version = "1.36.0", features = ["fs"] }
uuid = { version = "1.7.0", features = ["serde", "v3"] }
md-5 = "0.10.6"
async-trait = "0.1.78"

[dev-dependencies]
env_logger = "0.11.2"
tokio = { version = "1.36.0", features = ["full"] }
tokio = { version = "1.36.0", features = ["full"] }
6 changes: 4 additions & 2 deletions azalea-auth/examples/auth.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
use std::path::PathBuf;

use azalea_auth::MicrosoftAccount;

#[tokio::main]
async fn main() {
env_logger::init();

let cache_file = PathBuf::from("example_cache.json");

let auth_result = azalea_auth::auth(
let auth_result = MicrosoftAccount::new(
"[email protected]",
azalea_auth::AuthOpts {
azalea_auth::MicrosoftAuthOpts {
cache_file: Some(cache_file),
..Default::default()
},
Expand Down
23 changes: 21 additions & 2 deletions azalea-auth/examples/auth_manual.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,25 @@ async fn auth() -> Result<ProfileResponse, Box<dyn Error>> {
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?)
let xbl_auth = azalea_auth::auth_with_xbox_live(&client, &msa.data.access_token).await?;

let xsts_token = azalea_auth::obtain_xsts_for_minecraft(
&client,
&xbl_auth
.get()
.expect("Xbox Live auth token shouldn't have expired yet")
.token,
)
.await?;

// Minecraft auth
let mca =
azalea_auth::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(azalea_auth::get_profile(&client, &minecraft_access_token).await?)
}
10 changes: 5 additions & 5 deletions azalea-auth/examples/certificates.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
use std::path::PathBuf;

use azalea_auth::{account::Account, MicrosoftAccount};

#[tokio::main]
async fn main() {
env_logger::init();

let cache_file = PathBuf::from("example_cache.json");

let auth_result = azalea_auth::auth(
let auth_result = MicrosoftAccount::new(
"[email protected]",
azalea_auth::AuthOpts {
azalea_auth::MicrosoftAuthOpts {
cache_file: Some(cache_file),
..Default::default()
},
)
.await
.unwrap();

let certs = azalea_auth::certs::fetch_certificates(&auth_result.access_token)
.await
.unwrap();
let certs = auth_result.fetch_certificates().await.unwrap();

println!("{certs:?}");
}
36 changes: 36 additions & 0 deletions azalea-auth/src/account.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use async_trait::async_trait;
use uuid::Uuid;

use crate::{
certs::{Certificates, FetchCertificatesError},
sessionserver::ClientSessionServerError,
};

#[async_trait]
pub trait Account: std::fmt::Debug + Send + Sync + 'static {
async fn join(
&self,
public_key: &[u8],
private_key: &[u8],
server_id: &str,
) -> Result<(), ClientSessionServerError> {
let server_hash = azalea_crypto::hex_digest(&azalea_crypto::digest_data(
server_id.as_bytes(),
public_key,
private_key,
));
let uuid = self.get_uuid();

self.join_with_server_id_hash(uuid, server_hash).await
}
async fn join_with_server_id_hash(
&self,
uuid: Uuid,
server_hash: String,
) -> Result<(), ClientSessionServerError>;

async fn fetch_certificates(&self) -> Result<Option<Certificates>, FetchCertificatesError>;

fn get_username(&self) -> String;
fn get_uuid(&self) -> Uuid;
}
10 changes: 6 additions & 4 deletions azalea-auth/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use thiserror::Error;
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

use crate::{AccessTokenResponse, MinecraftAuthResponse, ProfileResponse, XboxLiveAuth};

#[derive(Debug, Error)]
pub enum CacheError {
#[error("Failed to read cache file: {0}")]
Expand All @@ -23,13 +25,13 @@ pub enum CacheError {
pub struct CachedAccount {
pub email: String,
/// Microsoft auth
pub msa: ExpiringValue<crate::auth::AccessTokenResponse>,
pub msa: ExpiringValue<AccessTokenResponse>,
/// Xbox Live auth
pub xbl: ExpiringValue<crate::auth::XboxLiveAuth>,
pub xbl: ExpiringValue<XboxLiveAuth>,
/// Minecraft auth
pub mca: ExpiringValue<crate::auth::MinecraftAuthResponse>,
pub mca: ExpiringValue<MinecraftAuthResponse>,
/// The user's Minecraft profile (i.e. username, UUID, skin)
pub profile: crate::auth::ProfileResponse,
pub profile: ProfileResponse,
}

#[derive(Deserialize, Serialize, Debug)]
Expand Down
66 changes: 1 addition & 65 deletions azalea-auth/src/certs.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use base64::Engine;
use chrono::{DateTime, Utc};
use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey};
use rsa::RsaPrivateKey;
use serde::Deserialize;
use thiserror::Error;

Expand All @@ -12,69 +11,6 @@ pub enum FetchCertificatesError {
Pkcs8(#[from] rsa::pkcs8::Error),
}

/// Fetch the Mojang-provided key-pair for your player, which is used for
/// cryptographically signing chat messages.
pub async fn fetch_certificates(
minecraft_access_token: &str,
) -> Result<Certificates, FetchCertificatesError> {
let client = reqwest::Client::new();

let res = client
.post("https://api.minecraftservices.com/player/certificates")
.header("Authorization", format!("Bearer {minecraft_access_token}"))
.send()
.await?
.json::<CertificatesResponse>()
.await?;
tracing::trace!("{:?}", res);

// using RsaPrivateKey::from_pkcs8_pem gives an error with decoding base64 so we
// just decode it ourselves

// remove the first and last lines of the private key
let private_key_pem_base64 = res
.key_pair
.private_key
.lines()
.skip(1)
.take_while(|line| !line.starts_with('-'))
.collect::<String>();
let private_key_der = base64::engine::general_purpose::STANDARD
.decode(private_key_pem_base64)
.unwrap();

let public_key_pem_base64 = res
.key_pair
.public_key
.lines()
.skip(1)
.take_while(|line| !line.starts_with('-'))
.collect::<String>();
let public_key_der = base64::engine::general_purpose::STANDARD
.decode(public_key_pem_base64)
.unwrap();

// the private key also contains the public key so it's basically a keypair
let private_key = RsaPrivateKey::from_pkcs8_der(&private_key_der).unwrap();

let certificates = Certificates {
private_key,
public_key_der,

signature_v1: base64::engine::general_purpose::STANDARD
.decode(&res.public_key_signature)
.unwrap(),
signature_v2: base64::engine::general_purpose::STANDARD
.decode(&res.public_key_signature_v2)
.unwrap(),

expires_at: res.expires_at,
refresh_after: res.refreshed_after,
};

Ok(certificates)
}

/// A chat signing certificate.
#[derive(Clone, Debug)]
pub struct Certificates {
Expand Down
6 changes: 4 additions & 2 deletions azalea-auth/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#![doc = include_str!("../README.md")]

mod auth;
pub mod account;
pub mod cache;
pub mod certs;
pub mod game_profile;
pub mod microsoft;
pub mod offline;
pub mod sessionserver;

pub use auth::*;
pub use microsoft::*;
pub use offline::*;
Loading