mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 14:26:04 +00:00
how did i not notice that i had the code duplicated
This commit is contained in:
parent
fe1fb2dbfd
commit
7faee59143
3 changed files with 4 additions and 314 deletions
|
@ -1,6 +1,6 @@
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let auth_result = azalea_auth::auth(azalea_auth::AuthOpts::default())
|
let auth_result = azalea_auth::auth("example@example.com", azalea_auth::AuthOpts::default())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
println!("{:?}", auth_result);
|
println!("{:?}", auth_result);
|
||||||
|
|
|
@ -31,9 +31,7 @@ pub struct AuthOpts {
|
||||||
/// The email is technically only used as a cache key, so it *could* be
|
/// 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
|
/// 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.
|
/// though, and in case the Microsoft API does start providing the real email.
|
||||||
pub async fn auth(email: &str, opts: Option<AuthOpts>) -> anyhow::Result<AuthResult> {
|
pub async fn auth(email: &str, opts: AuthOpts) -> anyhow::Result<AuthResult> {
|
||||||
let opts = opts.unwrap_or_default();
|
|
||||||
|
|
||||||
let cached_account = if let Some(cache_file) = &opts.cache_file && let Some(account) = cache::get_account_in_cache(&cache_file, email).await {
|
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)
|
Some(account)
|
||||||
} else { None };
|
} else { None };
|
||||||
|
|
|
@ -1,316 +1,8 @@
|
||||||
#![feature(let_chains)]
|
#![feature(let_chains)]
|
||||||
|
|
||||||
pub mod auth;
|
mod auth;
|
||||||
mod cache;
|
mod cache;
|
||||||
pub mod game_profile;
|
pub mod game_profile;
|
||||||
pub mod sessionserver;
|
pub mod sessionserver;
|
||||||
|
|
||||||
use anyhow::anyhow;
|
pub use auth::*;
|
||||||
use serde::Deserialize;
|
|
||||||
use serde_json::json;
|
|
||||||
use std::{collections::HashMap, time::Instant};
|
|
||||||
|
|
||||||
/// Ask the user to authenticate with Microsoft and return the user's Minecraft
|
|
||||||
/// access token. There's caching, so if the user has already authenticated
|
|
||||||
/// before then they won't have to log in manually again.
|
|
||||||
pub async fn auth(opts: AuthOpts) -> anyhow::Result<String> {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
|
|
||||||
let auth_token_res = interactive_get_ms_auth_token(&client).await?;
|
|
||||||
// TODO: cache this
|
|
||||||
println!("Got access token: {}", auth_token_res.access_token);
|
|
||||||
|
|
||||||
let xbl_auth = auth_with_xbox_live(&client, &auth_token_res.access_token).await?;
|
|
||||||
|
|
||||||
let xsts_token = obtain_xsts_for_minecraft(&client, &xbl_auth).await?;
|
|
||||||
|
|
||||||
let minecraft_access_token =
|
|
||||||
auth_with_minecraft(&client, &xbl_auth.user_hash, &xsts_token).await?;
|
|
||||||
|
|
||||||
if opts.check_ownership {
|
|
||||||
let has_game = check_ownership(&client, &minecraft_access_token).await?;
|
|
||||||
if !has_game {
|
|
||||||
panic!(
|
|
||||||
"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."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// let profile = get_profile(&client, &minecraft_access_token).await?;
|
|
||||||
|
|
||||||
Ok(minecraft_access_token)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AuthOpts {
|
|
||||||
/// Whether we should check if the user actually owns the game. This will
|
|
||||||
/// fail if the user has Xbox Game Pass! Note that this isn't really
|
|
||||||
/// necessary, since getting the user profile will check this anyways.
|
|
||||||
pub check_ownership: bool,
|
|
||||||
// /// Whether we should get the Minecraft profile data (i.e. username, uuid,
|
|
||||||
// /// skin, etc) for the player.
|
|
||||||
// pub get_profile: bool,
|
|
||||||
/// The file to store cached data in. If this is `None`, then no
|
|
||||||
/// caching will be done.
|
|
||||||
pub cache_dir: Option<std::path::PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AuthOpts {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
check_ownership: true,
|
|
||||||
// get_profile: true,
|
|
||||||
cache_dir: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct DeviceCodeResponse {
|
|
||||||
user_code: String,
|
|
||||||
device_code: String,
|
|
||||||
verification_uri: String,
|
|
||||||
expires_in: u64,
|
|
||||||
interval: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct AccessTokenResponse {
|
|
||||||
token_type: String,
|
|
||||||
expires_in: u64,
|
|
||||||
scope: String,
|
|
||||||
access_token: String,
|
|
||||||
refresh_token: String,
|
|
||||||
user_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub struct XboxLiveAuthResponse {
|
|
||||||
issue_instant: String,
|
|
||||||
not_after: String,
|
|
||||||
token: String,
|
|
||||||
display_claims: HashMap<String, Vec<HashMap<String, String>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Just the important data
|
|
||||||
pub struct XboxLiveAuth {
|
|
||||||
token: String,
|
|
||||||
user_hash: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct MinecraftAuthResponse {
|
|
||||||
username: String,
|
|
||||||
roles: Vec<String>,
|
|
||||||
access_token: String,
|
|
||||||
token_type: String,
|
|
||||||
expires_in: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct GameOwnershipResponse {
|
|
||||||
items: Vec<GameOwnershipItem>,
|
|
||||||
signature: String,
|
|
||||||
key_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct GameOwnershipItem {
|
|
||||||
name: String,
|
|
||||||
signature: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct ProfileResponse {
|
|
||||||
pub id: String,
|
|
||||||
pub name: String,
|
|
||||||
pub skins: Vec<serde_json::Value>,
|
|
||||||
pub capes: Vec<serde_json::Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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<AccessTokenResponse> {
|
|
||||||
// nintendo switch (real)
|
|
||||||
let client_id = "00000000441cc96b";
|
|
||||||
|
|
||||||
let res = client
|
|
||||||
.post("https://login.live.com/oauth20_connect.srf")
|
|
||||||
.form(&vec![
|
|
||||||
("scope", "service::user.auth.xboxlive.com::MBI_SSL"),
|
|
||||||
("client_id", client_id),
|
|
||||||
("response_type", "device_code"),
|
|
||||||
])
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json::<DeviceCodeResponse>()
|
|
||||||
.await?;
|
|
||||||
println!("{:?}", res);
|
|
||||||
println!(
|
|
||||||
"Go to {} and enter the code {}",
|
|
||||||
res.verification_uri, res.user_code
|
|
||||||
);
|
|
||||||
|
|
||||||
let access_token_response: AccessTokenResponse;
|
|
||||||
|
|
||||||
let expire_time = Instant::now() + std::time::Duration::from_secs(res.expires_in);
|
|
||||||
|
|
||||||
while Instant::now() < expire_time {
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(res.interval)).await;
|
|
||||||
|
|
||||||
println!("trying");
|
|
||||||
if let Ok(res) = client
|
|
||||||
.post(format!(
|
|
||||||
"https://login.live.com/oauth20_token.srf?client_id={}",
|
|
||||||
client_id
|
|
||||||
))
|
|
||||||
.form(&vec![
|
|
||||||
("client_id", client_id),
|
|
||||||
("device_code", &res.device_code),
|
|
||||||
("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
|
|
||||||
])
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json::<AccessTokenResponse>()
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
access_token_response = res;
|
|
||||||
return Ok(access_token_response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(anyhow!("Authentication timed out"))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn auth_with_xbox_live(
|
|
||||||
client: &reqwest::Client,
|
|
||||||
access_token: &str,
|
|
||||||
) -> anyhow::Result<XboxLiveAuth> {
|
|
||||||
let auth_json = json!({
|
|
||||||
"Properties": {
|
|
||||||
"AuthMethod": "RPS",
|
|
||||||
"SiteName": "user.auth.xboxlive.com",
|
|
||||||
// i thought this was supposed to be d={} but it doesn't work for
|
|
||||||
// me when i add it ??????
|
|
||||||
"RpsTicket": format!("{}", access_token)
|
|
||||||
},
|
|
||||||
"RelyingParty": "http://auth.xboxlive.com",
|
|
||||||
"TokenType": "JWT"
|
|
||||||
});
|
|
||||||
let payload = auth_json.to_string();
|
|
||||||
// let signature = sign(
|
|
||||||
// "https://user.auth.xboxlive.com/user/authenticate",
|
|
||||||
// "",
|
|
||||||
// &payload,
|
|
||||||
// )?;
|
|
||||||
println!("auth_json: {:#?}", auth_json);
|
|
||||||
let res = client
|
|
||||||
.post("https://user.auth.xboxlive.com/user/authenticate")
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.header("x-xbl-contract-version", "1")
|
|
||||||
// .header("Cache-Control", "no-store, must-revalidate, no-cache")
|
|
||||||
// .header("Signature", base64::encode(signature))
|
|
||||||
.body(payload)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json::<XboxLiveAuthResponse>()
|
|
||||||
.await?;
|
|
||||||
println!("got res: {:?}", res);
|
|
||||||
|
|
||||||
Ok(XboxLiveAuth {
|
|
||||||
token: res.token,
|
|
||||||
user_hash: res.display_claims["xui"].get(0).unwrap()["uhs"].clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn obtain_xsts_for_minecraft(
|
|
||||||
client: &reqwest::Client,
|
|
||||||
xbl_auth: &XboxLiveAuth,
|
|
||||||
) -> anyhow::Result<String> {
|
|
||||||
let res = client
|
|
||||||
.post("https://xsts.auth.xboxlive.com/xsts/authorize")
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.json(&json!({
|
|
||||||
"Properties": {
|
|
||||||
"SandboxId": "RETAIL",
|
|
||||||
"UserTokens": [xbl_auth.token]
|
|
||||||
},
|
|
||||||
"RelyingParty": "rp://api.minecraftservices.com/",
|
|
||||||
"TokenType": "JWT"
|
|
||||||
}))
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json::<XboxLiveAuthResponse>()
|
|
||||||
.await?;
|
|
||||||
println!("{:?}", res);
|
|
||||||
|
|
||||||
Ok(res.token)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn auth_with_minecraft(
|
|
||||||
client: &reqwest::Client,
|
|
||||||
user_hash: &str,
|
|
||||||
xsts_token: &str,
|
|
||||||
) -> anyhow::Result<String> {
|
|
||||||
let res = client
|
|
||||||
.post("https://api.minecraftservices.com/authentication/login_with_xbox")
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.json(&json!({
|
|
||||||
"identityToken": format!("XBL3.0 x={};{}", user_hash, xsts_token)
|
|
||||||
}))
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json::<MinecraftAuthResponse>()
|
|
||||||
.await?;
|
|
||||||
println!("{:?}", res);
|
|
||||||
|
|
||||||
Ok(res.access_token)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn check_ownership(
|
|
||||||
client: &reqwest::Client,
|
|
||||||
minecraft_access_token: &str,
|
|
||||||
) -> anyhow::Result<bool> {
|
|
||||||
let res = client
|
|
||||||
.get("https://api.minecraftservices.com/entitlements/mcstore")
|
|
||||||
.header(
|
|
||||||
"Authorization",
|
|
||||||
format!("Bearer {}", minecraft_access_token),
|
|
||||||
)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json::<GameOwnershipResponse>()
|
|
||||||
.await?;
|
|
||||||
println!("{:?}", res);
|
|
||||||
|
|
||||||
// TODO: we *should* check with mojang's public key that the signatures are right
|
|
||||||
|
|
||||||
Ok(!res.items.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_profile(
|
|
||||||
client: &reqwest::Client,
|
|
||||||
minecraft_access_token: &str,
|
|
||||||
) -> anyhow::Result<ProfileResponse> {
|
|
||||||
let res = client
|
|
||||||
.get("https://api.minecraftservices.com/minecraft/profile")
|
|
||||||
.header(
|
|
||||||
"Authorization",
|
|
||||||
format!("Bearer {}", minecraft_access_token),
|
|
||||||
)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json::<ProfileResponse>()
|
|
||||||
.await?;
|
|
||||||
println!("{:?}", res);
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue