From d94d730a446df63c6406c4e84635d737edcab744 Mon Sep 17 00:00:00 2001 From: Kevin Yue Date: Mon, 15 Apr 2024 08:27:33 -0400 Subject: [PATCH] feat: support default browser for CLI (#345) --- Cargo.lock | 2 +- Cargo.toml | 1 + apps/gpauth/Cargo.toml | 1 + .../gpauth/src}/browser_authenticator.rs | 0 apps/gpauth/src/cli.rs | 16 ++++++- apps/gpauth/src/main.rs | 1 + apps/gpclient/src/connect.rs | 45 +++++++++++++++++-- apps/gpclient/src/launch_gui.rs | 17 +++++++ apps/gpclient/src/main.rs | 1 + crates/gpapi/Cargo.toml | 2 - crates/gpapi/src/process/auth_launcher.rs | 19 +++++++- crates/gpapi/src/process/mod.rs | 2 - 12 files changed, 96 insertions(+), 11 deletions(-) rename {crates/gpapi/src/process => apps/gpauth/src}/browser_authenticator.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 7fbbacc..26ebd27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1439,7 +1439,6 @@ dependencies = [ "dotenvy_macro", "log", "md5", - "open", "redact-engine", "regex", "reqwest", @@ -1471,6 +1470,7 @@ dependencies = [ "gpapi", "html-escape", "log", + "open", "regex", "serde_json", "tauri", diff --git a/Cargo.toml b/Cargo.toml index c2f3dc1..7ab0040 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ compile-time = "0.2" serde_urlencoded = "0.7" md5="0.7" sha256="1" +open = "5" # Tauri dependencies tauri = { version = "1.5" } diff --git a/apps/gpauth/Cargo.toml b/apps/gpauth/Cargo.toml index a19a296..fa8f75f 100644 --- a/apps/gpauth/Cargo.toml +++ b/apps/gpauth/Cargo.toml @@ -22,3 +22,4 @@ html-escape = "0.2.13" webkit2gtk = "0.18.2" tauri = { workspace = true, features = ["http-all"] } compile-time.workspace = true +open.workspace = true diff --git a/crates/gpapi/src/process/browser_authenticator.rs b/apps/gpauth/src/browser_authenticator.rs similarity index 100% rename from crates/gpapi/src/process/browser_authenticator.rs rename to apps/gpauth/src/browser_authenticator.rs diff --git a/apps/gpauth/src/cli.rs b/apps/gpauth/src/cli.rs index 45729c8..1ab97f8 100644 --- a/apps/gpauth/src/cli.rs +++ b/apps/gpauth/src/cli.rs @@ -11,7 +11,10 @@ use serde_json::json; use tauri::{App, AppHandle, RunEvent}; use tempfile::NamedTempFile; -use crate::auth_window::{portal_prelogin, AuthWindow}; +use crate::{ + auth_window::{portal_prelogin, AuthWindow}, + browser_authenticator::BrowserAuthenticator, +}; const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", compile_time::date_str!(), ")"); @@ -37,6 +40,8 @@ struct Cli { ignore_tls_errors: bool, #[arg(long)] clean: bool, + #[arg(long)] + default_browser: bool, } impl Cli { @@ -56,6 +61,15 @@ impl Cli { None => portal_prelogin(&self.server, &gp_params).await?, }; + if self.default_browser { + let browser_auth = BrowserAuthenticator::new(&saml_request); + browser_auth.authenticate()?; + + info!("Please continue the authentication process in the default browser"); + + return Ok(()); + } + self.saml_request.replace(saml_request); let app = create_app(self.clone())?; diff --git a/apps/gpauth/src/main.rs b/apps/gpauth/src/main.rs index 74f86ec..2c8ba0b 100644 --- a/apps/gpauth/src/main.rs +++ b/apps/gpauth/src/main.rs @@ -1,6 +1,7 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] mod auth_window; +mod browser_authenticator; mod cli; #[tokio::main] diff --git a/apps/gpclient/src/connect.rs b/apps/gpclient/src/connect.rs index 085a056..b6cf650 100644 --- a/apps/gpclient/src/connect.rs +++ b/apps/gpclient/src/connect.rs @@ -19,8 +19,9 @@ use gpapi::{ use inquire::{Password, PasswordDisplayMode, Select, Text}; use log::info; use openconnect::Vpn; +use tokio::{io::AsyncReadExt, net::TcpListener}; -use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE}; +use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE, GP_CLIENT_PORT_FILE}; #[derive(Args)] pub(crate) struct ConnectArgs { @@ -60,6 +61,8 @@ pub(crate) struct ConnectArgs { hidpi: bool, #[arg(long, help = "Do not reuse the remembered authentication cookie")] clean: bool, + #[arg(long, help = "Use the default browser to authenticate")] + default_browser: bool, } impl ConnectArgs { @@ -240,7 +243,9 @@ impl<'a> ConnectHandler<'a> { match prelogin { Prelogin::Saml(prelogin) => { - SamlAuthLauncher::new(&self.args.server) + let use_default_browser = prelogin.support_default_browser() && self.args.default_browser; + + let cred = SamlAuthLauncher::new(&self.args.server) .gateway(is_gateway) .saml_request(prelogin.saml_request()) .user_agent(&self.args.user_agent) @@ -250,8 +255,21 @@ impl<'a> ConnectHandler<'a> { .fix_openssl(self.shared_args.fix_openssl) .ignore_tls_errors(self.shared_args.ignore_tls_errors) .clean(self.args.clean) + .default_browser(use_default_browser) .launch() - .await + .await?; + + if let Some(cred) = cred { + return Ok(cred); + } + + if !use_default_browser { + // This should never happen + unreachable!("SAML authentication failed without using the default browser"); + } + + info!("Waiting for the browser authentication to complete..."); + wait_credentials().await } Prelogin::Standard(prelogin) => { let prefix = if is_gateway { "Gateway" } else { "Portal" }; @@ -274,6 +292,27 @@ impl<'a> ConnectHandler<'a> { } } +async fn wait_credentials() -> anyhow::Result { + // Start a local server to receive the browser authentication data + let listener = TcpListener::bind("127.0.0.1:0").await?; + let port = listener.local_addr()?.port(); + + // Write the port to a file + fs::write(GP_CLIENT_PORT_FILE, port.to_string())?; + + info!("Listening authentication data on port {}", port); + let (mut socket, _) = listener.accept().await?; + + info!("Received the browser authentication data from the socket"); + let mut data = String::new(); + socket.read_to_string(&mut data).await?; + + // Remove the port file + fs::remove_file(GP_CLIENT_PORT_FILE)?; + + Credential::from_gpcallback(&data) +} + fn write_pid_file() { let pid = std::process::id(); diff --git a/apps/gpclient/src/launch_gui.rs b/apps/gpclient/src/launch_gui.rs index 03a5570..7c0bcf1 100644 --- a/apps/gpclient/src/launch_gui.rs +++ b/apps/gpclient/src/launch_gui.rs @@ -7,6 +7,9 @@ use gpapi::{ utils::{endpoint::http_endpoint, env_file, shutdown_signal}, }; use log::info; +use tokio::io::AsyncWriteExt; + +use crate::GP_CLIENT_PORT_FILE; #[derive(Args)] pub(crate) struct LaunchGuiArgs { @@ -78,6 +81,11 @@ impl<'a> LaunchGuiHandler<'a> { } async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> { + let _ = tokio::join!(feed_auth_data_gui(auth_data), feed_auth_data_cli(auth_data)); + Ok(()) +} + +async fn feed_auth_data_gui(auth_data: &str) -> anyhow::Result<()> { let service_endpoint = http_endpoint().await?; reqwest::Client::default() @@ -90,6 +98,15 @@ async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> { Ok(()) } +async fn feed_auth_data_cli(auth_data: &str) -> anyhow::Result<()> { + let port = tokio::fs::read_to_string(GP_CLIENT_PORT_FILE).await?; + let mut stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port.trim())).await?; + + stream.write_all(auth_data.as_bytes()).await?; + + Ok(()) +} + async fn try_active_gui() -> anyhow::Result<()> { let service_endpoint = http_endpoint().await?; diff --git a/apps/gpclient/src/main.rs b/apps/gpclient/src/main.rs index 41343ed..0c6e763 100644 --- a/apps/gpclient/src/main.rs +++ b/apps/gpclient/src/main.rs @@ -4,6 +4,7 @@ mod disconnect; mod launch_gui; pub(crate) const GP_CLIENT_LOCK_FILE: &str = "/var/run/gpclient.lock"; +pub(crate) const GP_CLIENT_PORT_FILE: &str = "/var/run/gpclient.port"; #[tokio::main] async fn main() { diff --git a/crates/gpapi/Cargo.toml b/crates/gpapi/Cargo.toml index af91e7e..67075bf 100644 --- a/crates/gpapi/Cargo.toml +++ b/crates/gpapi/Cargo.toml @@ -31,9 +31,7 @@ sha256.workspace = true tauri = { workspace = true, optional = true } clap = { workspace = true, optional = true } -open = { version = "5", optional = true } [features] tauri = ["dep:tauri"] clap = ["dep:clap"] -browser-auth = ["dep:open"] diff --git a/crates/gpapi/src/process/auth_launcher.rs b/crates/gpapi/src/process/auth_launcher.rs index fc012fe..837c91e 100644 --- a/crates/gpapi/src/process/auth_launcher.rs +++ b/crates/gpapi/src/process/auth_launcher.rs @@ -18,6 +18,7 @@ pub struct SamlAuthLauncher<'a> { fix_openssl: bool, ignore_tls_errors: bool, clean: bool, + default_browser: bool, } impl<'a> SamlAuthLauncher<'a> { @@ -33,6 +34,7 @@ impl<'a> SamlAuthLauncher<'a> { fix_openssl: false, ignore_tls_errors: false, clean: false, + default_browser: false, } } @@ -81,8 +83,13 @@ impl<'a> SamlAuthLauncher<'a> { self } + pub fn default_browser(mut self, default_browser: bool) -> Self { + self.default_browser = default_browser; + self + } + /// Launch the authenticator binary as the current user or SUDO_USER if available. - pub async fn launch(self) -> anyhow::Result { + pub async fn launch(self) -> anyhow::Result> { let mut auth_cmd = Command::new(GP_AUTH_BINARY); auth_cmd.arg(self.server); @@ -122,6 +129,10 @@ impl<'a> SamlAuthLauncher<'a> { auth_cmd.arg("--clean"); } + if self.default_browser { + auth_cmd.arg("--default-browser"); + } + let mut non_root_cmd = auth_cmd.into_non_root()?; let output = non_root_cmd .kill_on_drop(true) @@ -130,12 +141,16 @@ impl<'a> SamlAuthLauncher<'a> { .wait_with_output() .await?; + if self.default_browser { + return Ok(None); + } + let Ok(auth_result) = serde_json::from_slice::(&output.stdout) else { bail!("Failed to parse auth data") }; match auth_result { - SamlAuthResult::Success(auth_data) => Ok(Credential::from(auth_data)), + SamlAuthResult::Success(auth_data) => Ok(Some(Credential::from(auth_data))), SamlAuthResult::Failure(msg) => bail!(msg), } } diff --git a/crates/gpapi/src/process/mod.rs b/crates/gpapi/src/process/mod.rs index 5dbb18c..b5beb20 100644 --- a/crates/gpapi/src/process/mod.rs +++ b/crates/gpapi/src/process/mod.rs @@ -2,8 +2,6 @@ pub(crate) mod command_traits; pub(crate) mod gui_helper_launcher; pub mod auth_launcher; -#[cfg(feature = "browser-auth")] -pub mod browser_authenticator; pub mod gui_launcher; pub mod hip_launcher; pub mod service_launcher;