feat: basic auth implementation

This commit is contained in:
uku 2024-07-28 22:48:02 +02:00
parent 3e0b846a3a
commit de92a0f05a
Signed by: uku
SSH key fingerprint: SHA256:4P0aN6M8ajKukNi6aPOaX0LacanGYtlfjmN+m/sHY/o
4 changed files with 1537 additions and 11 deletions

1323
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,5 +5,15 @@ edition = "2021"
[dependencies] [dependencies]
color-eyre = "0.6.3" color-eyre = "0.6.3"
oauth2 = "4.4.2"
reqwest = { version = "0.12.5", default-features = false, features = [
"http2",
"charset",
"json",
"rustls-tls",
] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.38.0", features = ["full"] }
tracing = "0.1.40" tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "chrono"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter", "chrono"] }

188
src/auth.rs Normal file
View file

@ -0,0 +1,188 @@
use std::time::Duration;
use color_eyre::{
eyre::{eyre, OptionExt, Report, Result},
Section,
};
use oauth2::{
basic::BasicClient, reqwest::async_http_client, AuthUrl, ClientId, DeviceAuthorizationUrl,
Scope, StandardDeviceAuthorizationResponse, TokenResponse, TokenUrl,
};
use serde::Deserialize;
use serde_json::json;
const CLIENT_ID: &str = "39bd526b-1897-42c9-b652-8b987550ab89";
const DEVICE_CODE_URL: &str = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
const MSA_AUTHORIZE_URL: &str = "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize";
const MSA_TOKEN_URL: &str = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
const XBOX_USER_AUTH_URL: &str = "https://user.auth.xboxlive.com/user/authenticate";
const XBOX_XSTS_AUTHORIZE_URL: &str = "https://xsts.auth.xboxlive.com/xsts/authorize";
const MINECRAFT_AUTH_URL: &str = "https://api.minecraftservices.com/authentication/login_with_xbox";
const MINECRAFT_PROFILE_URL: &str = "https://api.minecraftservices.com/minecraft/profile";
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct XboxLiveResponse {
token: String,
/// { "xui": [{"uhs": "userhash"}] }
display_claims: serde_json::Value,
}
impl XboxLiveResponse {
fn get_userhash(&self) -> Result<String> {
self.display_claims
.get("xui")
.and_then(|v| v.get(0))
.and_then(|v| v.get("uhs"))
.and_then(|v| v.as_str())
.map(std::borrow::ToOwned::to_owned)
.ok_or_eyre("Could not fetch user hash")
.with_note(|| format!("DisplayClaims value: {}", self.display_claims))
}
}
#[derive(Debug, Deserialize)]
struct MinecraftLoginResponse {
access_token: String,
}
#[derive(Debug, Deserialize)]
pub struct MinecraftProfile {
pub id: String,
pub name: String,
}
// TODO better error handling !!!!
pub async fn login() -> Result<MinecraftProfile> {
let access_token = request_msa_token().await?;
let (xbox_token, userhash) = auth_xbox_live(&access_token).await?;
let (xsts_token, xsts_userhash) = fetch_xsts_token(&xbox_token).await?;
if userhash != xsts_userhash {
return Err(eyre!("user hashes do not match!").with_note(|| {
format!("xbox live userhash: {userhash} / xsts userhash: {xsts_userhash}")
}));
}
let mc_token = login_to_mc(&xsts_token, &userhash).await?;
get_minecraft_profile(&mc_token).await
}
async fn request_msa_token() -> Result<String> {
let client = BasicClient::new(
ClientId::new(CLIENT_ID.to_string()),
None,
AuthUrl::new(MSA_AUTHORIZE_URL.to_string())?,
Some(TokenUrl::new(MSA_TOKEN_URL.to_string())?),
)
.set_device_authorization_url(DeviceAuthorizationUrl::new(DEVICE_CODE_URL.to_string())?);
let details: StandardDeviceAuthorizationResponse = client
.exchange_device_code()?
.add_scope(Scope::new("XboxLive.signin offline_access".to_string()))
.request_async(async_http_client)
.await?;
tracing::info!(
"url: {} code: {}",
**details.verification_uri(),
details.user_code().secret()
);
let token_result = client
.exchange_device_access_token(&details)
.request_async(
async_http_client,
tokio::time::sleep,
Some(Duration::from_secs(300)),
)
.await?;
tracing::info!("fetched msa account token");
Ok(token_result.access_token().secret().clone())
}
async fn auth_xbox_live(msa_token: &str) -> Result<(String, String)> {
tracing::info!("fetching xbox token");
let json = json!({
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": format!("d={msa_token}") // idek man
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
});
let xbox_res = crate::HTTP_CLIENT
.post(XBOX_USER_AUTH_URL)
.json(&json)
.send()
.await?
.json::<XboxLiveResponse>()
.await?;
let userhash = xbox_res.get_userhash()?;
Ok((xbox_res.token, userhash))
}
async fn fetch_xsts_token(xbox_token: &str) -> Result<(String, String)> {
tracing::info!("fetching xbox xsts token");
let json = json!({
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [xbox_token]
},
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT"
});
let xsts_res = crate::HTTP_CLIENT
.post(XBOX_XSTS_AUTHORIZE_URL)
.json(&json)
.send()
.await?
.json::<XboxLiveResponse>()
.await?;
let userhash = xsts_res.get_userhash()?;
Ok((xsts_res.token, userhash))
}
async fn login_to_mc(xsts_token: &str, userhash: &str) -> Result<String> {
tracing::info!("logging in to minecraft");
let json = json!({
"identityToken": format!("XBL3.0 x={userhash};{xsts_token}")
});
let mc_res = crate::HTTP_CLIENT
.post(MINECRAFT_AUTH_URL)
.json(&json)
.send()
.await?
.json::<MinecraftLoginResponse>()
.await?;
Ok(mc_res.access_token)
}
async fn get_minecraft_profile(mc_token: &str) -> Result<MinecraftProfile> {
tracing::info!("fetching player profile");
crate::HTTP_CLIENT
.get(MINECRAFT_PROFILE_URL)
.bearer_auth(mc_token)
.send()
.await?
.json::<MinecraftProfile>()
.await
.map_err(Report::from)
}

View file

@ -1,8 +1,11 @@
#![warn(clippy::pedantic)] #![warn(clippy::pedantic)]
use std::fs::File; mod auth;
use std::{fs::File, sync::LazyLock};
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
use reqwest::header::{HeaderMap, USER_AGENT};
use tracing::level_filters::LevelFilter; use tracing::level_filters::LevelFilter;
use tracing_subscriber::{ use tracing_subscriber::{
fmt::{self, time::ChronoLocal}, fmt::{self, time::ChronoLocal},
@ -11,13 +14,33 @@ use tracing_subscriber::{
EnvFilter, Layer, EnvFilter, Layer,
}; };
fn main() -> Result<()> { const VERSION: &str = env!("CARGO_PKG_VERSION");
static HTTP_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
let mut headers = HeaderMap::new();
headers.insert(
USER_AGENT,
format!("uku3lig/uklient/{VERSION}").parse().unwrap(),
);
reqwest::Client::builder()
.default_headers(headers)
.build()
.unwrap()
});
#[tokio::main]
async fn main() -> Result<()> {
color_eyre::install()?; color_eyre::install()?;
crate::init_tracing()?; crate::init_tracing()?;
tracing::info!("starting..."); tracing::info!("starting...");
tracing::debug!("starting debug"); tracing::debug!("starting debug");
let profile = auth::login().await?;
tracing::info!("logged in as {} ({})", profile.name, profile.id);
Ok(()) Ok(())
} }