From 6fe397e86809b9be1d315ad9805280d74195c286 Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Wed, 26 Nov 2025 00:52:53 +0800 Subject: [PATCH] open auth portal --- src-tauri/.env.example | 4 + src-tauri/Cargo.lock | 126 ++++++++++++++++++++++-- src-tauri/Cargo.toml | 9 +- src-tauri/src/app.rs | 1 + src-tauri/src/core/models/app_config.rs | 9 ++ src-tauri/src/core/services/auth.rs | 66 +++++++++++++ src-tauri/src/core/services/mod.rs | 2 +- src-tauri/src/core/state.rs | 24 ++++- src-tauri/src/lib.rs | 1 + 9 files changed, 228 insertions(+), 14 deletions(-) create mode 100644 src-tauri/.env.example create mode 100644 src-tauri/src/core/services/auth.rs diff --git a/src-tauri/.env.example b/src-tauri/.env.example new file mode 100644 index 0000000..0754541 --- /dev/null +++ b/src-tauri/.env.example @@ -0,0 +1,4 @@ +API_BASE_URL=http://127.0.0.1:3000 +AUTH_URL=https://auth.example.com/realms/friendolls/protocol/openid-connect/auth +JWT_AUDIENCE=friendolls-desktop +REDIRECT_URI=http://localhost:8582/callback diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 495adb2..81e73e5 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -656,6 +656,27 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "dbus" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190b6255e8ab55a7b568df5a883e9497edc3e4821c06396612048b430e5ad1e9" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "dbus-secret-service" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" +dependencies = [ + "dbus", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.5" @@ -774,6 +795,12 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dpi" version = "0.1.2" @@ -1016,11 +1043,15 @@ dependencies = [ name = "friendolls-desktop" version = "0.1.0" dependencies = [ + "base64 0.22.1", "device_query", - "once_cell", + "dotenvy", + "keyring", + "rand 0.9.2", "reqwest", "serde", "serde_json", + "sha2", "tauri", "tauri-build", "tauri-plugin-global-shortcut", @@ -1028,6 +1059,7 @@ dependencies = [ "tauri-plugin-positioner", "tokio", "ts-rs", + "url", ] [[package]] @@ -1047,6 +1079,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1943,6 +1976,21 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "byteorder", + "dbus-secret-service", + "log", + "security-framework 2.11.1", + "security-framework 3.5.1", + "windows-sys 0.60.2", + "zeroize", +] + [[package]] name = "kuchikiki" version = "0.8.8-speedreader" @@ -1991,6 +2039,15 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "libdbus-sys" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f" +dependencies = [ + "pkg-config", +] + [[package]] name = "libloading" version = "0.7.4" @@ -2160,7 +2217,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -3014,6 +3071,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -3034,6 +3101,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -3052,6 +3129,15 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -3166,6 +3252,7 @@ dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2", @@ -3369,6 +3456,19 @@ dependencies = [ "security-framework-sys", ] +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework-sys" version = "2.15.0" @@ -4542,9 +4642,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "ts-rs" -version = "11.0.1" +version = "11.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef1b7a6d914a34127ed8e1fa927eb7088903787bcded4fa3eef8f85ee1568be" +checksum = "4994acea2522cd2b3b85c1d9529a55991e3ad5e25cdcd3de9d505972c4379424" dependencies = [ "thiserror 2.0.17", "ts-rs-macros", @@ -4552,9 +4652,9 @@ dependencies = [ [[package]] name = "ts-rs-macros" -version = "11.0.1" +version = "11.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d4ed7b4c18cc150a6a0a1e9ea1ecfa688791220781af6e119f9599a8502a0a" +checksum = "ee6ff59666c9cbaec3533964505d39154dc4e0a56151fdea30a09ed0301f62e2" dependencies = [ "proc-macro2", "quote", @@ -5683,6 +5783,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] [[package]] name = "zerotrie" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 91d0187..0240e20 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,10 +25,15 @@ serde_json = "1" tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] } tauri-plugin-global-shortcut = "2" tauri-plugin-positioner = "2" -reqwest = { version = "0.12.23", features = ["json", "native-tls"] } +reqwest = { version = "0.12.23", features = ["json", "native-tls", "blocking"] } ts-rs = "11.0.1" device_query = "4.0.1" -once_cell = "1.21.3" +dotenvy = "0.15.7" +keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service"] } +url = "2.5.7" +rand = "0.9.2" +sha2 = "0.10.9" +base64 = "0.22.1" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-global-shortcut = "2" diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs index aa39406..2989c7b 100644 --- a/src-tauri/src/app.rs +++ b/src-tauri/src/app.rs @@ -41,4 +41,5 @@ pub async fn initialize_session() { webview_window.open_devtools(); println!("Scene window initialized."); + crate::core::services::auth::get_auth_code(); } diff --git a/src-tauri/src/core/models/app_config.rs b/src-tauri/src/core/models/app_config.rs index 3ab205b..18749cb 100644 --- a/src-tauri/src/core/models/app_config.rs +++ b/src-tauri/src/core/models/app_config.rs @@ -1,8 +1,17 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; +#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] +#[ts(export)] +pub struct AuthConfig { + pub audience: String, + pub auth_url: String, + pub redirect_uri: String, +} + #[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] #[ts(export)] pub struct AppConfig { pub api_base_url: Option, + pub auth: AuthConfig, } diff --git a/src-tauri/src/core/services/auth.rs b/src-tauri/src/core/services/auth.rs new file mode 100644 index 0000000..72facd5 --- /dev/null +++ b/src-tauri/src/core/services/auth.rs @@ -0,0 +1,66 @@ +use crate::{core::state::FDOLL, lock_r, APP_HANDLE}; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use rand::{distr::Alphanumeric, Rng}; +use sha2::{Digest, Sha256}; +use tauri_plugin_opener::OpenerExt; + +/// Generate a random code verifier (PKCE spec: 43 to 128 chars, here defaulting to 64) +fn generate_code_verifier(length: usize) -> String { + rand::rng() + .sample_iter(&Alphanumeric) + .take(length) + .map(char::from) + .collect() +} + +/// Generate code challenge from a code verifier +fn generate_code_challenge(code_verifier: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(code_verifier.as_bytes()); + let result = hasher.finalize(); + URL_SAFE_NO_PAD.encode(&result) +} + +/// Returns the auth pass object, including +/// access token, refresh token, expire time etc. +#[allow(dead_code)] +pub fn get_tokens() { + todo!(); +} + +/// Opens the auth portal in the browser, +/// and returns auth code after user logged in. +pub fn get_auth_code() { + let app_config = lock_r!(FDOLL) + .app_config + .clone() + .expect("Invalid app config"); + + let opener = APP_HANDLE.get().unwrap().opener(); + + let code_verifier = generate_code_verifier(64); + let code_challenge = generate_code_challenge(&code_verifier); + let state = generate_code_verifier(16); + + let mut url = url::Url::parse(&app_config.auth.auth_url.as_str()).expect("Invalid app config"); + url.query_pairs_mut() + .append_pair("client_id", &app_config.auth.audience.as_str()) + .append_pair("response_type", "code") + .append_pair("redirect_uri", &app_config.auth.redirect_uri.as_str()) + .append_pair("scope", "openid email profile") + .append_pair("state", &state) + .append_pair("code_challenge", &code_challenge) + .append_pair("code_challenge_method", "S256"); + + match opener.open_url(url, None::<&str>) { + Ok(_) => (), + Err(e) => panic!("Failed to open auth portal: {}", e), + } +} + +/// Accepts a refresh token and +/// returns a new access token. +#[allow(dead_code)] +pub fn refresh_token() { + todo!(); +} diff --git a/src-tauri/src/core/services/mod.rs b/src-tauri/src/core/services/mod.rs index 8b13789..0e4a05d 100644 --- a/src-tauri/src/core/services/mod.rs +++ b/src-tauri/src/core/services/mod.rs @@ -1 +1 @@ - +pub mod auth; diff --git a/src-tauri/src/core/state.rs b/src-tauri/src/core/state.rs index 18c9a0f..4be93ce 100644 --- a/src-tauri/src/core/state.rs +++ b/src-tauri/src/core/state.rs @@ -1,7 +1,13 @@ // in app-core/src/state.rs -use crate::{core::models::app_config::AppConfig, lock_w}; +use crate::{ + core::models::app_config::{AppConfig, AuthConfig}, + lock_w, +}; use reqwest::Client; -use std::sync::{Arc, LazyLock, RwLock}; +use std::{ + env, + sync::{Arc, LazyLock, RwLock}, +}; #[derive(Default)] pub struct AppState { @@ -10,7 +16,6 @@ pub struct AppState { } // Global application state -// FDOLL = Multiplayer Todo App // Read / write this state via the `lock_r!` / `lock_w!` macros from `fdoll-core::utilities` pub static FDOLL: LazyLock>> = LazyLock::new(|| Arc::new(RwLock::new(AppState::default()))); @@ -18,9 +23,18 @@ pub static FDOLL: LazyLock>> = pub fn init_fdoll_state() { { let mut guard = lock_w!(FDOLL); + dotenvy::dotenv().ok(); guard.app_config = Some(AppConfig { - api_base_url: Some("http://sandbox:3000".to_string()), + api_base_url: Some(env::var("API_BASE_URL").expect("API_BASE_URL must be set")), + auth: AuthConfig { + audience: env::var("JWT_AUDIENCE").expect("JWT_AUDIENCE must be set"), + auth_url: env::var("AUTH_URL").expect("AUTH_URL must be set"), + redirect_uri: env::var("REDIRECT_URI").expect("REDIRECT_URI must be set"), + }, }); - guard.http_client = reqwest::Client::new(); + guard.http_client = reqwest::ClientBuilder::new() + .redirect(reqwest::redirect::Policy::none()) + .build() + .expect("Client should build"); } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6571d91..548dc2b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -37,6 +37,7 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_positioner::init()) + .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![channel_cursor_positions]) .setup(|app| { APP_HANDLE