mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 06:16:04 +00:00
auth works!
This commit is contained in:
parent
8e86450947
commit
74288b55c3
11 changed files with 161 additions and 50 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -116,7 +116,6 @@ dependencies = [
|
|||
name = "azalea-auth"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"azalea-buf",
|
||||
"azalea-crypto",
|
||||
"chrono",
|
||||
|
|
|
@ -8,7 +8,6 @@ version = "0.1.0"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.65"
|
||||
azalea-buf = {path = "../azalea-buf", version = "^0.1.0"}
|
||||
azalea-crypto = {path = "../azalea-crypto", version = "^0.1.0"}
|
||||
chrono = {version = "0.4.22", default-features = false}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
//! Handle Minecraft (Xbox) authentication.
|
||||
|
||||
use crate::cache::{self, CachedAccount, ExpiringValue};
|
||||
use anyhow::{anyhow, bail};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
@ -10,6 +9,7 @@ use std::{
|
|||
path::PathBuf,
|
||||
time::{Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AuthOpts {
|
||||
|
@ -25,13 +25,36 @@ pub struct AuthOpts {
|
|||
pub cache_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AuthError {
|
||||
#[error(
|
||||
"The Minecraft API is indicating that you don't own the game. \
|
||||
If you're using Xbox Game Pass, set `check_ownership` to false in the auth options."
|
||||
)]
|
||||
DoesNotOwnGame,
|
||||
#[error("Error getting Microsoft auth token: {0}")]
|
||||
GetMicrosoftAuthToken(#[from] GetMicrosoftAuthTokenError),
|
||||
#[error("Error refreshing Microsoft auth token: {0}")]
|
||||
RefreshMicrosoftAuthToken(#[from] RefreshMicrosoftAuthTokenError),
|
||||
#[error("Error getting Xbox Live auth token: {0}")]
|
||||
GetXboxLiveAuthToken(#[from] MinecraftXstsAuthError),
|
||||
#[error("Error getting Minecraft profile: {0}")]
|
||||
GetMinecraftProfile(#[from] GetProfileError),
|
||||
#[error("Error checking ownership: {0}")]
|
||||
CheckOwnership(#[from] CheckOwnershipError),
|
||||
#[error("Error getting Minecraft auth token: {0}")]
|
||||
GetMinecraftAuthToken(#[from] MinecraftAuthError),
|
||||
#[error("Error authenticating with Xbox Live: {0}")]
|
||||
GetXboxLiveAuth(#[from] XboxLiveAuthError),
|
||||
}
|
||||
|
||||
/// Authenticate with authenticate with Microsoft. If the data isn't cached,
|
||||
/// they'll be asked to go to log into Microsoft in a web page.
|
||||
///
|
||||
/// 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.
|
||||
pub async fn auth(email: &str, opts: AuthOpts) -> anyhow::Result<AuthResult> {
|
||||
pub async fn auth(email: &str, opts: AuthOpts) -> Result<AuthResult, AuthError> {
|
||||
let cached_account = if let Some(cache_file) = &opts.cache_file && let Some(account) = cache::get_account_in_cache(&cache_file, email).await {
|
||||
Some(account)
|
||||
} else { None };
|
||||
|
@ -82,17 +105,14 @@ pub async fn auth(email: &str, opts: AuthOpts) -> anyhow::Result<AuthResult> {
|
|||
if opts.check_ownership {
|
||||
let has_game = check_ownership(&client, &minecraft_access_token).await?;
|
||||
if !has_game {
|
||||
bail!(
|
||||
"The Minecraft API is indicating that you don't own the game. \
|
||||
If you're using Xbox Game Pass, set `check_ownership` to false in the auth options."
|
||||
);
|
||||
return Err(AuthError::DoesNotOwnGame);
|
||||
}
|
||||
}
|
||||
|
||||
profile = get_profile(&client, &minecraft_access_token).await?;
|
||||
|
||||
if let Some(cache_file) = opts.cache_file {
|
||||
cache::set_account_in_cache(
|
||||
if let Err(e) = cache::set_account_in_cache(
|
||||
&cache_file,
|
||||
email,
|
||||
CachedAccount {
|
||||
|
@ -103,7 +123,9 @@ pub async fn auth(email: &str, opts: AuthOpts) -> anyhow::Result<AuthResult> {
|
|||
profile: profile.clone(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
.await {
|
||||
log::warn!("Error while caching auth data: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,10 +214,18 @@ pub struct ProfileResponse {
|
|||
// nintendo switch (so it works for accounts that are under 18 years old)
|
||||
const CLIENT_ID: &str = "00000000441cc96b";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum GetMicrosoftAuthTokenError {
|
||||
#[error("Http error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
#[error("Authentication timed out")]
|
||||
Timeout,
|
||||
}
|
||||
|
||||
/// Asks the user to go to a webpage and log in with Microsoft.
|
||||
async fn interactive_get_ms_auth_token(
|
||||
client: &reqwest::Client,
|
||||
) -> anyhow::Result<ExpiringValue<AccessTokenResponse>> {
|
||||
) -> Result<ExpiringValue<AccessTokenResponse>, GetMicrosoftAuthTokenError> {
|
||||
let res = client
|
||||
.post("https://login.live.com/oauth20_connect.srf")
|
||||
.form(&vec![
|
||||
|
@ -247,13 +277,19 @@ async fn interactive_get_ms_auth_token(
|
|||
}
|
||||
}
|
||||
|
||||
Err(anyhow!("Authentication timed out"))
|
||||
Err(GetMicrosoftAuthTokenError::Timeout)
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RefreshMicrosoftAuthTokenError {
|
||||
#[error("Http error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
}
|
||||
|
||||
async fn refresh_ms_auth_token(
|
||||
client: &reqwest::Client,
|
||||
refresh_token: &str,
|
||||
) -> anyhow::Result<ExpiringValue<AccessTokenResponse>> {
|
||||
) -> Result<ExpiringValue<AccessTokenResponse>, RefreshMicrosoftAuthTokenError> {
|
||||
let access_token_response = client
|
||||
.post("https://login.live.com/oauth20_token.srf")
|
||||
.form(&vec![
|
||||
|
@ -278,10 +314,18 @@ async fn refresh_ms_auth_token(
|
|||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum XboxLiveAuthError {
|
||||
#[error("Http error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
#[error("Invalid expiry date: {0}")]
|
||||
InvalidExpiryDate(String),
|
||||
}
|
||||
|
||||
async fn auth_with_xbox_live(
|
||||
client: &reqwest::Client,
|
||||
access_token: &str,
|
||||
) -> anyhow::Result<ExpiringValue<XboxLiveAuth>> {
|
||||
) -> Result<ExpiringValue<XboxLiveAuth>, XboxLiveAuthError> {
|
||||
let auth_json = json!({
|
||||
"Properties": {
|
||||
"AuthMethod": "RPS",
|
||||
|
@ -311,12 +355,7 @@ async fn auth_with_xbox_live(
|
|||
|
||||
// not_after looks like 2020-12-21T19:52:08.4463796Z
|
||||
let expires_at = DateTime::parse_from_rfc3339(&res.not_after)
|
||||
.map_err(|_| {
|
||||
anyhow!(
|
||||
"Failed to parse not_after date from Xbox Live: {}",
|
||||
res.not_after
|
||||
)
|
||||
})?
|
||||
.map_err(|e| XboxLiveAuthError::InvalidExpiryDate(format!("{}: {}", res.not_after, e)))?
|
||||
.with_timezone(&Utc)
|
||||
.timestamp() as u64;
|
||||
Ok(ExpiringValue {
|
||||
|
@ -328,10 +367,16 @@ async fn auth_with_xbox_live(
|
|||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MinecraftXstsAuthError {
|
||||
#[error("Http error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
}
|
||||
|
||||
async fn obtain_xsts_for_minecraft(
|
||||
client: &reqwest::Client,
|
||||
xbl_auth_token: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
) -> Result<String, MinecraftXstsAuthError> {
|
||||
let res = client
|
||||
.post("https://xsts.auth.xboxlive.com/xsts/authorize")
|
||||
.header("Accept", "application/json")
|
||||
|
@ -352,11 +397,17 @@ async fn obtain_xsts_for_minecraft(
|
|||
Ok(res.token)
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MinecraftAuthError {
|
||||
#[error("Http error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
}
|
||||
|
||||
async fn auth_with_minecraft(
|
||||
client: &reqwest::Client,
|
||||
user_hash: &str,
|
||||
xsts_token: &str,
|
||||
) -> anyhow::Result<ExpiringValue<MinecraftAuthResponse>> {
|
||||
) -> Result<ExpiringValue<MinecraftAuthResponse>, MinecraftAuthError> {
|
||||
let res = client
|
||||
.post("https://api.minecraftservices.com/authentication/login_with_xbox")
|
||||
.header("Accept", "application/json")
|
||||
|
@ -377,10 +428,16 @@ async fn auth_with_minecraft(
|
|||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CheckOwnershipError {
|
||||
#[error("Http error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
}
|
||||
|
||||
async fn check_ownership(
|
||||
client: &reqwest::Client,
|
||||
minecraft_access_token: &str,
|
||||
) -> anyhow::Result<bool> {
|
||||
) -> Result<bool, CheckOwnershipError> {
|
||||
let res = client
|
||||
.get("https://api.minecraftservices.com/entitlements/mcstore")
|
||||
.header(
|
||||
|
@ -399,10 +456,16 @@ async fn check_ownership(
|
|||
Ok(!res.items.is_empty())
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum GetProfileError {
|
||||
#[error("Http error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
}
|
||||
|
||||
async fn get_profile(
|
||||
client: &reqwest::Client,
|
||||
minecraft_access_token: &str,
|
||||
) -> anyhow::Result<ProfileResponse> {
|
||||
) -> Result<ProfileResponse, GetProfileError> {
|
||||
let res = client
|
||||
.get("https://api.minecraftservices.com/minecraft/profile")
|
||||
.header(
|
||||
|
|
|
@ -46,13 +46,15 @@ pub async fn join(
|
|||
let mut encode_buffer = Uuid::encode_buffer();
|
||||
let undashed_uuid = uuid.simple().encode_lower(&mut encode_buffer);
|
||||
|
||||
let data = json!({
|
||||
"accessToken": access_token,
|
||||
"selectedProfile": undashed_uuid,
|
||||
"serverId": server_hash
|
||||
});
|
||||
println!("data: {:?}", data);
|
||||
let res = client
|
||||
.post("https://sessionserver.mojang.com/session/minecraft/join")
|
||||
.json(&json!({
|
||||
"accessToken": access_token,
|
||||
"selectedProfile": undashed_uuid,
|
||||
"serverId": server_hash
|
||||
}))
|
||||
.json(&data)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
//! Connect to Minecraft servers.
|
||||
|
||||
use crate::{client::JoinError, Client, Event};
|
||||
use crate::{client::JoinError, get_mc_dir, Client, Event};
|
||||
use azalea_protocol::ServerAddress;
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Something that can join Minecraft servers.
|
||||
pub struct Account {
|
||||
|
@ -22,6 +23,23 @@ impl Account {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn microsoft(email: &str) -> Result<Self, azalea_auth::AuthError> {
|
||||
let minecraft_dir = get_mc_dir::minecraft_dir().unwrap();
|
||||
let auth_result = azalea_auth::auth(
|
||||
email,
|
||||
azalea_auth::AuthOpts {
|
||||
cache_file: Some(minecraft_dir.join("azalea-auth.json")),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(Self {
|
||||
username: auth_result.profile.name,
|
||||
access_token: Some(auth_result.access_token),
|
||||
uuid: Some(Uuid::parse_str(&auth_result.profile.id).expect("Invalid UUID")),
|
||||
})
|
||||
}
|
||||
|
||||
/// Joins the Minecraft server on the given address using this account.
|
||||
pub async fn join(
|
||||
&self,
|
||||
|
|
|
@ -166,13 +166,12 @@ impl Client {
|
|||
&account
|
||||
.uuid
|
||||
.expect("Uuid must be present if access token is present."),
|
||||
e.secret_key,
|
||||
p,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// TODO: authenticate with the server here (authenticateServer)
|
||||
|
||||
conn.write(
|
||||
ServerboundKeyPacket {
|
||||
nonce_or_salt_signature: NonceOrSaltSignature::Nonce(
|
||||
|
@ -183,6 +182,7 @@ impl Client {
|
|||
.get(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
conn.set_encryption_key(e.secret_key);
|
||||
}
|
||||
ClientboundLoginPacket::LoginCompression(p) => {
|
||||
|
|
34
azalea-client/src/get_mc_dir.rs
Normal file
34
azalea-client/src/get_mc_dir.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
//! Find out where the user's .minecraft directory is.
|
||||
//!
|
||||
//! Used for the auth cache.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Return the location of the user's .minecraft directory.
|
||||
///
|
||||
/// Windows: `%appdata%\.minecraft`\
|
||||
/// Mac: `$HOME/Library/Application Support/minecraft`\
|
||||
/// Linux: `$HOME/.minecraft`
|
||||
///
|
||||
/// Anywhere else it'll return None.
|
||||
pub fn minecraft_dir() -> Option<PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let appdata = std::env::var("APPDATA").ok()?;
|
||||
Some(PathBuf::from(appdata).join(".minecraft"))
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let home = std::env::var("HOME").ok()?;
|
||||
Some(PathBuf::from(home).join("Library/Application Support/minecraft"))
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let home = std::env::var("HOME").ok()?;
|
||||
Some(PathBuf::from(home).join(".minecraft"))
|
||||
}
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
mod account;
|
||||
mod client;
|
||||
mod get_mc_dir;
|
||||
mod movement;
|
||||
pub mod ping;
|
||||
mod player;
|
||||
|
|
|
@ -18,8 +18,8 @@ fn generate_secret_key() -> [u8; 16] {
|
|||
pub fn digest_data(server_id: &[u8], public_key: &[u8], private_key: &[u8]) -> Vec<u8> {
|
||||
let mut digest = Sha1::new();
|
||||
digest.update(server_id);
|
||||
digest.update(public_key);
|
||||
digest.update(private_key);
|
||||
digest.update(public_key);
|
||||
digest.finalize().to_vec()
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,6 @@ pub struct ReadConnection<R: ProtocolPacket> {
|
|||
buffer: BytesMut,
|
||||
compression_threshold: Option<u32>,
|
||||
dec_cipher: Option<Aes128CfbDec>,
|
||||
private_key: Option<[u8; 16]>,
|
||||
_reading: PhantomData<R>,
|
||||
}
|
||||
|
||||
|
@ -33,7 +32,6 @@ pub struct WriteConnection<W: ProtocolPacket> {
|
|||
write_stream: OwnedWriteHalf,
|
||||
compression_threshold: Option<u32>,
|
||||
enc_cipher: Option<Aes128CfbEnc>,
|
||||
private_key: Option<[u8; 16]>,
|
||||
_writing: PhantomData<W>,
|
||||
}
|
||||
|
||||
|
@ -120,14 +118,12 @@ impl Connection<ClientboundHandshakePacket, ServerboundHandshakePacket> {
|
|||
buffer: BytesMut::new(),
|
||||
compression_threshold: None,
|
||||
dec_cipher: None,
|
||||
private_key: None,
|
||||
_reading: PhantomData,
|
||||
},
|
||||
writer: WriteConnection {
|
||||
write_stream,
|
||||
compression_threshold: None,
|
||||
enc_cipher: None,
|
||||
private_key: None,
|
||||
_writing: PhantomData,
|
||||
},
|
||||
})
|
||||
|
@ -159,8 +155,6 @@ impl Connection<ClientboundLoginPacket, ServerboundLoginPacket> {
|
|||
let (enc_cipher, dec_cipher) = azalea_crypto::create_cipher(&key);
|
||||
self.reader.dec_cipher = Some(dec_cipher);
|
||||
self.writer.enc_cipher = Some(enc_cipher);
|
||||
self.reader.private_key = Some(key);
|
||||
self.writer.private_key = Some(key);
|
||||
}
|
||||
|
||||
pub fn game(self) -> Connection<ClientboundGamePacket, ServerboundGamePacket> {
|
||||
|
@ -195,12 +189,13 @@ impl Connection<ClientboundLoginPacket, ServerboundLoginPacket> {
|
|||
&self,
|
||||
access_token: &str,
|
||||
uuid: &Uuid,
|
||||
private_key: [u8; 16],
|
||||
packet: ClientboundHelloPacket,
|
||||
) -> Result<(), SessionServerError> {
|
||||
azalea_auth::sessionserver::join(
|
||||
access_token,
|
||||
&packet.public_key,
|
||||
&self.writer.private_key.expect("encryption key not set"),
|
||||
&private_key,
|
||||
uuid,
|
||||
&packet.server_id,
|
||||
)
|
||||
|
@ -226,14 +221,12 @@ where
|
|||
buffer: connection.reader.buffer,
|
||||
compression_threshold: connection.reader.compression_threshold,
|
||||
dec_cipher: connection.reader.dec_cipher,
|
||||
private_key: connection.reader.private_key,
|
||||
_reading: PhantomData,
|
||||
},
|
||||
writer: WriteConnection {
|
||||
compression_threshold: connection.writer.compression_threshold,
|
||||
write_stream: connection.writer.write_stream,
|
||||
enc_cipher: connection.writer.enc_cipher,
|
||||
private_key: connection.writer.private_key,
|
||||
_writing: PhantomData,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -7,10 +7,10 @@ use std::sync::Arc;
|
|||
struct State {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
let account = Account::offline("bot");
|
||||
let account = Account::microsoft("example@example.com").await?;
|
||||
|
||||
azalea::start(azalea::Options {
|
||||
account,
|
||||
|
@ -21,12 +21,14 @@ async fn main() {
|
|||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn handle(bot: Client, event: Arc<Event>, _state: Arc<Mutex<State>>) -> anyhow::Result<()> {
|
||||
if let Event::Tick = *event {
|
||||
bot.jump();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle(bot: Client, event: Arc<Event>, _state: Arc<Mutex<State>>) -> anyhow::Result<()> {
|
||||
// if let Event::Tick = *event {
|
||||
// bot.jump();
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue