Completely rework backend, now work to authentificate user, still need a bit of work to got MC Game Profile

This commit is contained in:
Quentin Legot 2023-01-08 19:33:11 +01:00
parent dd89761fcc
commit 9e518db87c
4 changed files with 144 additions and 80 deletions

2
src-tauri/Cargo.lock generated
View File

@ -6,7 +6,9 @@ version = 3
name = "Launcher-tauri" name = "Launcher-tauri"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"anyhow",
"log4rs", "log4rs",
"rand 0.8.5",
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",

View File

@ -20,9 +20,11 @@ tauri = {version = "1.2", features = ["api-all"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
uuid = "1.2.2" uuid = "1.2.2"
log4rs = "1.2.0" log4rs = "1.2.0"
reqwest = "0.11.13" reqwest = { version = "0.11.13", default-features = true, features = ["json"] }
urlencoding = "2.1.2" urlencoding = "2.1.2"
warp = "0.3.3" warp = "0.3.3"
anyhow = "1.0.66"
rand = "0.8.5"
[features] [features]
# by default Tauri runs in production mode # by default Tauri runs in production mode

View File

@ -1,11 +1,13 @@
use std::{fmt, net::TcpListener, io::Result}; use std::{fmt, net::TcpListener, sync::Arc};
use log4rs::Handle; use rand::{thread_rng, Rng};
use reqwest::header::{CONTENT_TYPE, CONNECTION}; use reqwest::{header::{CONTENT_TYPE, CONNECTION, ACCEPT}, Client};
use serde_json::{Value, json};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use urlencoding::encode; use urlencoding::encode;
use serde::Deserialize; use serde::Deserialize;
use warp::{Filter, http::Response}; use warp::{Filter, http::Response};
use anyhow::{bail, Result, anyhow};
pub enum Prompt { pub enum Prompt {
Login, Login,
@ -25,49 +27,51 @@ impl fmt::Display for Prompt {
} }
} }
pub struct Token { pub struct OauthToken {
client_id: String, client_id: String,
redirect: String, redirect: String,
prompt: Prompt prompt: Arc<Prompt>
}
struct AccessRefreshToken {
access_token: String,
refresh_token: String
}
struct XboxAuthData {
token: String,
uhs: String
} }
#[derive(Deserialize, Clone, Debug)] #[derive(Deserialize, Clone, Debug)]
struct ReceivedCode { pub struct ReceivedCode {
pub code: String, pub code: String,
pub state: String, pub state: String,
}
pub struct Authentification {
logger: Handle
} }
pub struct Authentification;
impl Authentification { impl Authentification {
pub fn new(logger: Handle) -> Self { fn mojang_auth_token(prompt: Prompt, port: u16) -> OauthToken {
Authentification { logger } OauthToken {
} client_id: String::from("89db80b6-8a97-4d00-97e8-48b18f377871"),
redirect: String::from(format!("http://localhost:{}/api/auth/redirect", port)),
pub fn mojang_auth_token(prompt: Prompt) -> Token { prompt: Arc::new(prompt)
Token {
client_id: String::from("00000000402b5328"),
redirect: String::from("https://localhost:PORT/api/auth/redirect"),
prompt
} }
} }
pub fn create_link(token: Token)-> String { fn create_link(token: &OauthToken, state: &String)-> String {
format!("https://login.live.com/oauth20_authorize.srf?client_id={}&response_type=code&redirect_uri={}&scope=Xboxlive.signin+Xboxlive.offline_access&prompt={}&state={}", token.client_id, encode(token.redirect.as_str()), token.prompt, state)
format!("https://login.live.com/oauth20_authorize.srf?client_id={}&response_type=code&redirect_uri={}&scope=XboxLive.signin%20offline_access&prompt={}", token.client_id, encode(token.redirect.as_str()), token.prompt)
} }
pub fn create_link_from_prompt(prompt: Prompt) -> String { async fn fetch_oauth2_token(prompt: Prompt, app: tauri::AppHandle) -> Result<(ReceivedCode, OauthToken)> {
Self::create_link(Self::mojang_auth_token(prompt)) let state: String = thread_rng()
} .sample_iter(&rand::distributions::Alphanumeric)
.take(16)
.map(char::from)
.collect();
pub async fn launch(prompt: Prompt, app: tauri::AppHandle) -> Result<()> {
// let reqwest_client = ReqwestClient::new();
let token = Self::mojang_auth_token(prompt);
let reqwest_client = reqwest::Client::new();
let mut port_holder = None; let mut port_holder = None;
let mut port = 0; let mut port = 0;
for i in 7878..65535 { for i in 7878..65535 {
@ -78,10 +82,10 @@ impl Authentification {
} }
}; };
if port_holder.is_none() { if port_holder.is_none() {
Err(()) bail!("Cannot create port")
} }
let redirect_uri = token.redirect.replace("PORT", &port.to_string()); let token_data = Self::mojang_auth_token(prompt, port);
let link = Self::create_link_from_prompt(token.prompt); let link = Self::create_link(&token_data, &state);
let second_window = tauri::WindowBuilder::new( let second_window = tauri::WindowBuilder::new(
&app, &app,
@ -89,13 +93,91 @@ impl Authentification {
tauri::WindowUrl::External(link.parse().unwrap()) tauri::WindowUrl::External(link.parse().unwrap())
).build().expect("Failed to build window"); ).build().expect("Failed to build window");
let received = Self::listen(port_holder.unwrap()).await?; let received = Self::listen(port_holder.unwrap()).await?;
second_window.close()?;
if received.state != state {
bail!("CSRF check fail")
}
Ok((received, token_data))
}
// fn create_link_from_prompt(prompt: Prompt) -> String {
// Self::create_link(&Self::mojang_auth_token(prompt))
// }
async fn fetch_token(oauth_token: ReceivedCode, token_data: OauthToken, reqwest_client: &Client) -> Result<AccessRefreshToken> {
let request_body = format!("\
client_id={}\
&code={}\
&grant_type=authorization_code\
&redirect_uri={}", token_data.client_id, oauth_token.code, token_data.redirect);
let received : Value = reqwest_client
.post("https://login.live.com/oauth20_token.srf")
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(request_body.into_bytes())
.send()
.await?
.json()
.await?;
let token = AccessRefreshToken {
access_token: received["access_token"].to_string(),
refresh_token: received["refresh_token"].to_string()
};
Ok(token)
}
async fn auth_xbox_live(access_refresh_token: AccessRefreshToken, reqwest_client: &Client) -> Result<XboxAuthData> {
let request_body: Value = json!({
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": format!("d={}", access_refresh_token.access_token)
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
});
let received: Value = reqwest_client
.post("https://user.auth.xboxlive.com/user/authenticate")
.header(CONTENT_TYPE, "application/json")
.header(ACCEPT, "application/json")
.json(&request_body)
.send()
.await?
.json()
.await?;
let xbox_auth = XboxAuthData {
token: received["Token"].to_string(),
uhs: received["DisplayClaims"]["xui"][0]["uhs"].to_string()
};
Ok(xbox_auth)
}
async fn fetch_xsts_token() -> Result<()> {
bail!("Not implemented yet")
}
pub async fn launch(prompt: Prompt, app: tauri::AppHandle) -> Result<()> {
let reqwest_client = Client::new();
let oauth_token = Self::fetch_oauth2_token(prompt, app).await?;
let access_refresh_token = Self::fetch_token(oauth_token.0, oauth_token.1, &reqwest_client).await?;
let xbox_auth = Self::auth_xbox_live(access_refresh_token, &reqwest_client).await?;
Ok(()) Ok(())
} }
pub async fn listen(port_holder: TcpListener) -> Result<ReceivedCode> { async fn listen(port_holder: TcpListener) -> Result<ReceivedCode> {
let (tx, mut rx) = mpsc::channel::<ReceivedCode>(2); let (tx, mut rx) = mpsc::channel::<ReceivedCode>(2);
let route = warp::query::<ReceivedCode>() let route = warp::query::<ReceivedCode>()
.and(warp::header::<String>("accept-language")) .and(warp::header::<String>("accept-language"))
.and_then(move |r: ReceivedCode, accept_lang: String| { .and_then(move |r: ReceivedCode, accept_lang: String| {
@ -108,11 +190,11 @@ impl Authentification {
if !accept_lang.is_empty() { if !accept_lang.is_empty() {
let langs = accept_lang.split(","); let langs = accept_lang.split(",");
for lang in langs { for lang in langs {
if lang.starts_with("fr_FR") { if lang.starts_with("fr_") { // also include canadian, belgium, etc. french
message = "Vous pouvez maintenant fermer l'onglet!"; message = "Vous pouvez maintenant fermer l'onglet!";
break; break;
} }
else if lang.starts_with("en") { else {
message = "You can close this tab now!"; message = "You can close this tab now!";
break; break;
} }
@ -127,49 +209,23 @@ impl Authentification {
.header(CONNECTION, "close") .header(CONNECTION, "close")
.body(format!("<h1>{}</h1>", message)) .body(format!("<h1>{}</h1>", message))
) )
} } else {
else {
Err(warp::reject()) Err(warp::reject())
} }
} }
}); });
Ok(ReceivedCode { code: "".to_string(), state: "".to_string() }) let port = port_holder.local_addr()?.port();
drop(port_holder);
let server = warp::serve(route).bind(([127, 0, 0, 1], port));
tokio::select! {
_ = server => bail!("Serve went down unexpectedly!"),
r = rx.recv() => r.ok_or(anyhow!("Can't receive code!")),
_ = async {
tokio::time::sleep(tokio::time::Duration::from_secs(120)).await;
} => bail!("Wait for too much time"),
}
} }
pub async fn xbox_auth(&self, access_token: String) -> () {
let str = format!(r#"{{
"Properties": {{
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": "d={}"
}},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
}}
"#, access_token);
let req = reqwest::Client::new();
let r_xbox_live = req.post("https://user.auth.xboxlive.com/user/authenticate")
.body(str)
.header(reqwest::header::CONTENT_TYPE, "application/json")
.header(reqwest::header::ACCEPT, "application/json")
.send().await;
match r_xbox_live {
Ok(response) => {
let content = response.text().await;
match content {
Ok(text) => {
println!("Sucess: {:?}", text);
},
Err(err) => {
eprintln!("error 2: {:?}", err);
}
};
},
Err(err) => {
eprintln!("Error 1: {:?}", err);
}
};
}
} }

View File

@ -6,6 +6,7 @@
pub mod authentification; pub mod authentification;
use authentification::{Authentification, Prompt}; use authentification::{Authentification, Prompt};
use anyhow::Result;
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command] #[tauri::command]
@ -14,9 +15,12 @@ fn greet(name: &str) -> String {
} }
#[tauri::command] #[tauri::command]
async fn second_window(app: tauri::AppHandle, window: tauri::Window) -> Result<(), String> { async fn second_window(app: tauri::AppHandle, _window: tauri::Window) -> Result<(), String> {
Authentification::launch(Prompt::SelectAccount, app); let result = Authentification::launch(Prompt::SelectAccount, app).await;
Ok(()) match result {
Ok(_) => Ok(()),
Err(err) => Err(err.to_string())
}
} }
#[tokio::main] #[tokio::main]