feat: basic auth implementation
This commit is contained in:
parent
3e0b846a3a
commit
de92a0f05a
4 changed files with 1537 additions and 11 deletions
1323
Cargo.lock
generated
1323
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
10
Cargo.toml
10
Cargo.toml
|
@ -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
188
src/auth.rs
Normal 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)
|
||||||
|
}
|
27
src/main.rs
27
src/main.rs
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue