From 9e518db87c4c5cc41696c45d6b5c1b439eb34bb7 Mon Sep 17 00:00:00 2001 From: Quentin Legot Date: Sun, 8 Jan 2023 19:33:11 +0100 Subject: [PATCH] Completely rework backend, now work to authentificate user, still need a bit of work to got MC Game Profile --- src-tauri/Cargo.lock | 2 + src-tauri/Cargo.toml | 4 +- src-tauri/src/authentification/mod.rs | 208 ++++++++++++++++---------- src-tauri/src/main.rs | 10 +- 4 files changed, 144 insertions(+), 80 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ba3485b..fde0e73 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6,7 +6,9 @@ version = 3 name = "Launcher-tauri" version = "0.0.0" dependencies = [ + "anyhow", "log4rs", + "rand 0.8.5", "reqwest", "serde", "serde_json", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a7bf207..8080b6c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -20,9 +20,11 @@ tauri = {version = "1.2", features = ["api-all"] } tokio = { version = "1", features = ["full"] } uuid = "1.2.2" log4rs = "1.2.0" -reqwest = "0.11.13" +reqwest = { version = "0.11.13", default-features = true, features = ["json"] } urlencoding = "2.1.2" warp = "0.3.3" +anyhow = "1.0.66" +rand = "0.8.5" [features] # by default Tauri runs in production mode diff --git a/src-tauri/src/authentification/mod.rs b/src-tauri/src/authentification/mod.rs index cf657d8..b0aa034 100644 --- a/src-tauri/src/authentification/mod.rs +++ b/src-tauri/src/authentification/mod.rs @@ -1,11 +1,13 @@ -use std::{fmt, net::TcpListener, io::Result}; +use std::{fmt, net::TcpListener, sync::Arc}; -use log4rs::Handle; -use reqwest::header::{CONTENT_TYPE, CONNECTION}; +use rand::{thread_rng, Rng}; +use reqwest::{header::{CONTENT_TYPE, CONNECTION, ACCEPT}, Client}; +use serde_json::{Value, json}; use tokio::sync::mpsc; use urlencoding::encode; use serde::Deserialize; use warp::{Filter, http::Response}; +use anyhow::{bail, Result, anyhow}; pub enum Prompt { Login, @@ -25,49 +27,51 @@ impl fmt::Display for Prompt { } } -pub struct Token { +pub struct OauthToken { client_id: String, redirect: String, - prompt: Prompt + prompt: Arc +} + +struct AccessRefreshToken { + access_token: String, + refresh_token: String +} + +struct XboxAuthData { + token: String, + uhs: String } #[derive(Deserialize, Clone, Debug)] - struct ReceivedCode { - pub code: String, - pub state: String, - } - -pub struct Authentification { - logger: Handle +pub struct ReceivedCode { + pub code: String, + pub state: String, } +pub struct Authentification; + impl Authentification { - pub fn new(logger: Handle) -> Self { - Authentification { logger } - } - - pub fn mojang_auth_token(prompt: Prompt) -> Token { - Token { - client_id: String::from("00000000402b5328"), - redirect: String::from("https://localhost:PORT/api/auth/redirect"), - prompt + fn mojang_auth_token(prompt: Prompt, port: u16) -> OauthToken { + OauthToken { + client_id: String::from("89db80b6-8a97-4d00-97e8-48b18f377871"), + redirect: String::from(format!("http://localhost:{}/api/auth/redirect", port)), + prompt: Arc::new(prompt) } } - pub fn create_link(token: Token)-> String { - - 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) + 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) } - pub fn create_link_from_prompt(prompt: Prompt) -> String { - Self::create_link(Self::mojang_auth_token(prompt)) - } + async fn fetch_oauth2_token(prompt: Prompt, app: tauri::AppHandle) -> Result<(ReceivedCode, OauthToken)> { + 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 = 0; for i in 7878..65535 { @@ -78,24 +82,102 @@ impl Authentification { } }; if port_holder.is_none() { - Err(()) + bail!("Cannot create port") } - let redirect_uri = token.redirect.replace("PORT", &port.to_string()); - let link = Self::create_link_from_prompt(token.prompt); - + let token_data = Self::mojang_auth_token(prompt, port); + let link = Self::create_link(&token_data, &state); + let second_window = tauri::WindowBuilder::new( &app, "externam", tauri::WindowUrl::External(link.parse().unwrap()) ).build().expect("Failed to build window"); 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 { + 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 { + 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(()) } - pub async fn listen(port_holder: TcpListener) -> Result { + async fn listen(port_holder: TcpListener) -> Result { let (tx, mut rx) = mpsc::channel::(2); + let route = warp::query::() .and(warp::header::("accept-language")) .and_then(move |r: ReceivedCode, accept_lang: String| { @@ -108,11 +190,11 @@ impl Authentification { if !accept_lang.is_empty() { let langs = accept_lang.split(","); 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!"; break; } - else if lang.starts_with("en") { + else { message = "You can close this tab now!"; break; } @@ -127,49 +209,23 @@ impl Authentification { .header(CONNECTION, "close") .body(format!("

{}

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