mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	Support SSO using default browser
This commit is contained in:
		| @@ -28,7 +28,9 @@ uzers.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"] | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| use anyhow::bail; | ||||
| use regex::Regex; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| @@ -37,6 +39,32 @@ impl SamlAuthData { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn parse_html(html: &str) -> anyhow::Result<SamlAuthData> { | ||||
|     match parse_xml_tag(html, "saml-auth-status") { | ||||
|       Some(saml_status) if saml_status == "1" => { | ||||
|         let username = parse_xml_tag(html, "saml-username"); | ||||
|         let prelogin_cookie = parse_xml_tag(html, "prelogin-cookie"); | ||||
|         let portal_userauthcookie = parse_xml_tag(html, "portal-userauthcookie"); | ||||
|  | ||||
|         if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) { | ||||
|           return Ok(SamlAuthData::new( | ||||
|             username.unwrap(), | ||||
|             prelogin_cookie, | ||||
|             portal_userauthcookie, | ||||
|           )); | ||||
|         } | ||||
|  | ||||
|         bail!("Found invalid auth data in HTML"); | ||||
|       } | ||||
|       Some(status) => { | ||||
|         bail!("Found invalid SAML status {} in HTML", status); | ||||
|       } | ||||
|       None => { | ||||
|         bail!("No auth data found in HTML"); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn username(&self) -> &str { | ||||
|     &self.username | ||||
|   } | ||||
| @@ -61,3 +89,10 @@ impl SamlAuthData { | ||||
|     username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid) | ||||
|   } | ||||
| } | ||||
|  | ||||
| pub fn parse_xml_tag(html: &str, tag: &str) -> Option<String> { | ||||
|   let re = Regex::new(&format!("<{}>(.*)</{}>", tag, tag)).unwrap(); | ||||
|   re.captures(html) | ||||
|     .and_then(|captures| captures.get(1)) | ||||
|     .map(|m| m.as_str().to_string()) | ||||
| } | ||||
|   | ||||
| @@ -3,7 +3,7 @@ use std::collections::HashMap; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use specta::Type; | ||||
|  | ||||
| use crate::auth::SamlAuthData; | ||||
| use crate::{auth::SamlAuthData, utils::base64::decode_to_string}; | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| @@ -151,6 +151,17 @@ pub enum Credential { | ||||
| } | ||||
|  | ||||
| impl Credential { | ||||
|   /// Create a credential from a globalprotectcallback:<base64 encoded string> | ||||
|   pub fn parse_gpcallback(auth_data: &str) -> anyhow::Result<Self> { | ||||
|     // Remove the surrounding quotes | ||||
|     let auth_data = auth_data.trim_matches('"'); | ||||
|     let auth_data = auth_data.trim_start_matches("globalprotectcallback:"); | ||||
|     let auth_data = decode_to_string(auth_data)?; | ||||
|     let auth_data = SamlAuthData::parse_html(&auth_data)?; | ||||
|  | ||||
|     Self::try_from(auth_data) | ||||
|   } | ||||
|  | ||||
|   pub fn username(&self) -> &str { | ||||
|     match self { | ||||
|       Credential::Password(cred) => cred.username(), | ||||
|   | ||||
| @@ -50,6 +50,7 @@ pub struct GpParams { | ||||
|   client_version: Option<String>, | ||||
|   computer: String, | ||||
|   ignore_tls_errors: bool, | ||||
|   prefer_default_browser: bool, | ||||
| } | ||||
|  | ||||
| impl GpParams { | ||||
| @@ -69,6 +70,10 @@ impl GpParams { | ||||
|     self.ignore_tls_errors | ||||
|   } | ||||
|  | ||||
|   pub fn prefer_default_browser(&self) -> bool { | ||||
|     self.prefer_default_browser | ||||
|   } | ||||
|  | ||||
|   pub(crate) fn to_params(&self) -> HashMap<&str, &str> { | ||||
|     let mut params: HashMap<&str, &str> = HashMap::new(); | ||||
|     let client_os = self.client_os.as_str(); | ||||
| @@ -88,9 +93,10 @@ impl GpParams { | ||||
|       params.insert("os-version", os_version); | ||||
|     } | ||||
|  | ||||
|     if let Some(client_version) = &self.client_version { | ||||
|       params.insert("clientgpversion", client_version); | ||||
|     } | ||||
|     // NOTE: Do not include clientgpversion for now | ||||
|     // if let Some(client_version) = &self.client_version { | ||||
|     //   params.insert("clientgpversion", client_version); | ||||
|     // } | ||||
|  | ||||
|     params | ||||
|   } | ||||
| @@ -103,6 +109,7 @@ pub struct GpParamsBuilder { | ||||
|   client_version: Option<String>, | ||||
|   computer: String, | ||||
|   ignore_tls_errors: bool, | ||||
|   prefer_default_browser: bool, | ||||
| } | ||||
|  | ||||
| impl GpParamsBuilder { | ||||
| @@ -114,6 +121,7 @@ impl GpParamsBuilder { | ||||
|       client_version: Default::default(), | ||||
|       computer: whoami::hostname(), | ||||
|       ignore_tls_errors: false, | ||||
|       prefer_default_browser: false, | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -147,6 +155,11 @@ impl GpParamsBuilder { | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn prefer_default_browser(&mut self, prefer_default_browser: bool) -> &mut Self { | ||||
|     self.prefer_default_browser = prefer_default_browser; | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn build(&self) -> GpParams { | ||||
|     GpParams { | ||||
|       user_agent: self.user_agent.clone(), | ||||
| @@ -155,6 +168,7 @@ impl GpParamsBuilder { | ||||
|       client_version: self.client_version.clone(), | ||||
|       computer: self.computer.clone(), | ||||
|       ignore_tls_errors: self.ignore_tls_errors, | ||||
|       prefer_default_browser: self.prefer_default_browser, | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -26,6 +26,7 @@ const REQUIRED_PARAMS: [&str; 8] = [ | ||||
| pub struct SamlPrelogin { | ||||
|   region: String, | ||||
|   saml_request: String, | ||||
|   support_default_browser: bool, | ||||
| } | ||||
|  | ||||
| impl SamlPrelogin { | ||||
| @@ -36,6 +37,10 @@ impl SamlPrelogin { | ||||
|   pub fn saml_request(&self) -> &str { | ||||
|     &self.saml_request | ||||
|   } | ||||
|  | ||||
|   pub fn support_default_browser(&self) -> bool { | ||||
|     self.support_default_browser | ||||
|   } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Type, Clone)] | ||||
| @@ -86,14 +91,14 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel | ||||
|   info!("Portal prelogin, user_agent: {}", user_agent); | ||||
|  | ||||
|   let portal = normalize_server(portal)?; | ||||
|   let prelogin_url = format!( | ||||
|     "{}/global-protect/prelogin.esp?kerberos-support=yes", | ||||
|     portal | ||||
|   ); | ||||
|   let prelogin_url = format!("{}/global-protect/prelogin.esp", portal); | ||||
|   let mut params = gp_params.to_params(); | ||||
|  | ||||
|   params.insert("tmp", "tmp"); | ||||
|   params.insert("default-browser", "0"); | ||||
|   params.insert("cas-support", "yes"); | ||||
|   if gp_params.prefer_default_browser() { | ||||
|     params.insert("default-browser", "1"); | ||||
|   } | ||||
|  | ||||
|   params.retain(|k, _| { | ||||
|     REQUIRED_PARAMS | ||||
| @@ -125,12 +130,18 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel | ||||
|  | ||||
|   let saml_method = xml::get_child_text(&doc, "saml-auth-method"); | ||||
|   let saml_request = xml::get_child_text(&doc, "saml-request"); | ||||
|   let saml_default_browser = xml::get_child_text(&doc, "saml-default-browser"); | ||||
|   // Check if the prelogin response is SAML | ||||
|   if saml_method.is_some() && saml_request.is_some() { | ||||
|     let saml_request = base64::decode_to_string(&saml_request.unwrap())?; | ||||
|     let support_default_browser = saml_default_browser | ||||
|       .map(|s| s.to_lowercase() == "yes") | ||||
|       .unwrap_or(false); | ||||
|  | ||||
|     let saml_prelogin = SamlPrelogin { | ||||
|       region, | ||||
|       saml_request, | ||||
|       support_default_browser, | ||||
|     }; | ||||
|  | ||||
|     return Ok(Prelogin::Saml(saml_prelogin)); | ||||
|   | ||||
							
								
								
									
										34
									
								
								crates/gpapi/src/process/browser_authenticator.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								crates/gpapi/src/process/browser_authenticator.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| use std::{env::temp_dir, io::Write}; | ||||
|  | ||||
| pub struct BrowserAuthenticator<'a> { | ||||
|   auth_request: &'a str, | ||||
| } | ||||
|  | ||||
| impl BrowserAuthenticator<'_> { | ||||
|   pub fn new(auth_request: &str) -> BrowserAuthenticator { | ||||
|     BrowserAuthenticator { auth_request } | ||||
|   } | ||||
|  | ||||
|   pub fn authenticate(&self) -> anyhow::Result<()> { | ||||
|     if self.auth_request.starts_with("http") { | ||||
|       open::that_detached(self.auth_request)?; | ||||
|     } else { | ||||
|       let html_file = temp_dir().join("gpauth.html"); | ||||
|       let mut file = std::fs::File::create(&html_file)?; | ||||
|  | ||||
|       file.write_all(self.auth_request.as_bytes())?; | ||||
|  | ||||
|       open::that_detached(html_file)?; | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
|   } | ||||
| } | ||||
|  | ||||
| impl Drop for BrowserAuthenticator<'_> { | ||||
|   fn drop(&mut self) { | ||||
|     // Cleanup the temporary file | ||||
|     let html_file = temp_dir().join("gpauth.html"); | ||||
|     let _ = std::fs::remove_file(html_file); | ||||
|   } | ||||
| } | ||||
| @@ -1,5 +1,7 @@ | ||||
| pub(crate) mod command_traits; | ||||
|  | ||||
| pub mod auth_launcher; | ||||
| #[cfg(feature = "browser-auth")] | ||||
| pub mod browser_authenticator; | ||||
| pub mod gui_launcher; | ||||
| pub mod service_launcher; | ||||
|   | ||||
| @@ -7,4 +7,6 @@ use super::vpn_state::VpnState; | ||||
| pub enum WsEvent { | ||||
|   VpnState(VpnState), | ||||
|   ActiveGui, | ||||
|   /// External authentication data | ||||
|   AuthData(String), | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user