mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	Support CAS authentication
This commit is contained in:
		| @@ -7,6 +7,7 @@ use std::{ | |||||||
| use anyhow::bail; | use anyhow::bail; | ||||||
| use gpapi::{ | use gpapi::{ | ||||||
|   auth::SamlAuthData, |   auth::SamlAuthData, | ||||||
|  |   error::AuthDataParseError, | ||||||
|   gp_params::GpParams, |   gp_params::GpParams, | ||||||
|   portal::{prelogin, Prelogin}, |   portal::{prelogin, Prelogin}, | ||||||
|   utils::{redact::redact_uri, window::WindowExt}, |   utils::{redact::redact_uri, window::WindowExt}, | ||||||
| @@ -359,32 +360,29 @@ fn read_auth_data_from_html(html: &str) -> AuthResult { | |||||||
|     return Err(AuthDataError::Invalid); |     return Err(AuthDataError::Invalid); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   match parse_xml_tag(html, "saml-auth-status") { |   let auth_data = match SamlAuthData::from_html(html) { | ||||||
|     Some(saml_status) if saml_status == "1" => { |     Ok(auth_data) => Ok(auth_data), | ||||||
|       let username = parse_xml_tag(html, "saml-username"); |     Err(err) => { | ||||||
|       let prelogin_cookie = parse_xml_tag(html, "prelogin-cookie"); |       if let Some(gpcallback) = extract_gpcallback(html) { | ||||||
|       let portal_userauthcookie = parse_xml_tag(html, "portal-userauthcookie"); |         info!("Found gpcallback from html..."); | ||||||
|  |         SamlAuthData::from_gpcallback(gpcallback) | ||||||
|  |       } else { | ||||||
|  |         Err(err) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|       if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) { |   auth_data.map_err(|err| match err { | ||||||
|         return Ok(SamlAuthData::new( |     AuthDataParseError::NotFound => AuthDataError::NotFound, | ||||||
|           username.unwrap(), |     AuthDataParseError::Invalid => AuthDataError::Invalid, | ||||||
|           prelogin_cookie, |   }) | ||||||
|           portal_userauthcookie, |  | ||||||
|         )); |  | ||||||
| } | } | ||||||
|  |  | ||||||
|       info!("Found invalid auth data in HTML"); | fn extract_gpcallback(html: &str) -> Option<&str> { | ||||||
|       Err(AuthDataError::Invalid) |   let re = Regex::new(r#"globalprotectcallback:[^"]+"#).unwrap(); | ||||||
|     } |   re.captures(html) | ||||||
|     Some(status) => { |     .and_then(|captures| captures.get(0)) | ||||||
|       info!("Found invalid SAML status {} in HTML", status); |     .map(|m| m.as_str()) | ||||||
|       Err(AuthDataError::Invalid) |  | ||||||
|     } |  | ||||||
|     None => { |  | ||||||
|       info!("No auth data found in HTML"); |  | ||||||
|       Err(AuthDataError::NotFound) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSender<AuthResult>) { | fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSender<AuthResult>) { | ||||||
| @@ -437,13 +435,6 @@ fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSe | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| 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()) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| pub(crate) async fn clear_webview_cookies(window: &Window) -> anyhow::Result<()> { | pub(crate) async fn clear_webview_cookies(window: &Window) -> anyhow::Result<()> { | ||||||
|   let (tx, rx) = oneshot::channel::<Result<(), String>>(); |   let (tx, rx) = oneshot::channel::<Result<(), String>>(); | ||||||
|  |  | ||||||
| @@ -489,3 +480,27 @@ pub(crate) async fn clear_webview_cookies(window: &Window) -> anyhow::Result<()> | |||||||
|  |  | ||||||
|   rx.await?.map_err(|err| anyhow::anyhow!(err)) |   rx.await?.map_err(|err| anyhow::anyhow!(err)) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests { | ||||||
|  |   use super::*; | ||||||
|  |  | ||||||
|  |   #[test] | ||||||
|  |   fn extract_gpcallback_some() { | ||||||
|  |     let html = r#" | ||||||
|  |       <meta http-equiv="refresh" content="0; URL=globalprotectcallback:PGh0bWw+PCEtLSA8c"> | ||||||
|  |       <meta http-equiv="refresh" content="0; URL=globalprotectcallback:PGh0bWw+PCEtLSA8c"> | ||||||
|  |     "#; | ||||||
|  |  | ||||||
|  |     assert_eq!(extract_gpcallback(html), Some("globalprotectcallback:PGh0bWw+PCEtLSA8c")); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   #[test] | ||||||
|  |   fn extract_gpcallback_none() { | ||||||
|  |     let html = r#" | ||||||
|  |       <meta http-equiv="refresh" content="0; URL=PGh0bWw+PCEtLSA8c"> | ||||||
|  |     "#; | ||||||
|  |  | ||||||
|  |     assert_eq!(extract_gpcallback(html), None); | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,13 +1,17 @@ | |||||||
| use anyhow::anyhow; | use log::{info, warn}; | ||||||
| use regex::Regex; | use regex::Regex; | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
|  |  | ||||||
|  | use crate::{error::AuthDataParseError, utils::base64::decode_to_string}; | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Deserialize)] | #[derive(Debug, Serialize, Deserialize)] | ||||||
| #[serde(rename_all = "camelCase")] | #[serde(rename_all = "camelCase")] | ||||||
| pub struct SamlAuthData { | pub struct SamlAuthData { | ||||||
|  |   #[serde(alias = "un")] | ||||||
|   username: String, |   username: String, | ||||||
|   prelogin_cookie: Option<String>, |   prelogin_cookie: Option<String>, | ||||||
|   portal_userauthcookie: Option<String>, |   portal_userauthcookie: Option<String>, | ||||||
|  |   token: Option<String>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Deserialize)] | #[derive(Debug, Serialize, Deserialize)] | ||||||
| @@ -32,10 +36,11 @@ impl SamlAuthData { | |||||||
|       username, |       username, | ||||||
|       prelogin_cookie, |       prelogin_cookie, | ||||||
|       portal_userauthcookie, |       portal_userauthcookie, | ||||||
|  |       token: None, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   pub fn from_html(html: &str) -> anyhow::Result<SamlAuthData> { |   pub fn from_html(html: &str) -> anyhow::Result<SamlAuthData, AuthDataParseError> { | ||||||
|     match parse_xml_tag(html, "saml-auth-status") { |     match parse_xml_tag(html, "saml-auth-status") { | ||||||
|       Some(saml_status) if saml_status == "1" => { |       Some(saml_status) if saml_status == "1" => { | ||||||
|         let username = parse_xml_tag(html, "saml-username"); |         let username = parse_xml_tag(html, "saml-username"); | ||||||
| @@ -49,11 +54,36 @@ impl SamlAuthData { | |||||||
|             portal_userauthcookie, |             portal_userauthcookie, | ||||||
|           )) |           )) | ||||||
|         } else { |         } else { | ||||||
|           Err(anyhow!("Found invalid auth data in HTML")) |           Err(AuthDataParseError::Invalid) | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       Some(status) => Err(anyhow!("Found invalid SAML status {} in HTML", status)), |       Some(_) => Err(AuthDataParseError::Invalid), | ||||||
|       None => Err(anyhow!("No auth data found in HTML")), |       None => Err(AuthDataParseError::NotFound), | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn from_gpcallback(data: &str) -> anyhow::Result<SamlAuthData, AuthDataParseError> { | ||||||
|  |     let auth_data = data.trim_start_matches("globalprotectcallback:"); | ||||||
|  |  | ||||||
|  |     if auth_data.starts_with("cas-as") { | ||||||
|  |       info!("Got token auth data: {}", auth_data); | ||||||
|  |  | ||||||
|  |       let token_cred: SamlAuthData = serde_urlencoded::from_str(auth_data).map_err(|e| { | ||||||
|  |         warn!("Failed to parse token auth data: {}", e); | ||||||
|  |         AuthDataParseError::Invalid | ||||||
|  |       })?; | ||||||
|  |  | ||||||
|  |       Ok(token_cred) | ||||||
|  |     } else { | ||||||
|  |       info!("Parsing SAML auth data..."); | ||||||
|  |  | ||||||
|  |       let auth_data = decode_to_string(auth_data).map_err(|e| { | ||||||
|  |         warn!("Failed to decode SAML auth data: {}", e); | ||||||
|  |         AuthDataParseError::Invalid | ||||||
|  |       })?; | ||||||
|  |       let auth_data = Self::from_html(&auth_data)?; | ||||||
|  |  | ||||||
|  |       Ok(auth_data) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -65,6 +95,10 @@ impl SamlAuthData { | |||||||
|     self.prelogin_cookie.as_deref() |     self.prelogin_cookie.as_deref() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn token(&self) -> Option<&str> { | ||||||
|  |     self.token.as_deref() | ||||||
|  |   } | ||||||
|  |  | ||||||
|   pub fn check( |   pub fn check( | ||||||
|     username: &Option<String>, |     username: &Option<String>, | ||||||
|     prelogin_cookie: &Option<String>, |     prelogin_cookie: &Option<String>, | ||||||
| @@ -74,7 +108,16 @@ impl SamlAuthData { | |||||||
|     let prelogin_cookie_valid = prelogin_cookie.as_ref().is_some_and(|val| val.len() > 5); |     let prelogin_cookie_valid = prelogin_cookie.as_ref().is_some_and(|val| val.len() > 5); | ||||||
|     let portal_userauthcookie_valid = portal_userauthcookie.as_ref().is_some_and(|val| val.len() > 5); |     let portal_userauthcookie_valid = portal_userauthcookie.as_ref().is_some_and(|val| val.len() > 5); | ||||||
|  |  | ||||||
|     username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid) |     let is_valid = username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid); | ||||||
|  |  | ||||||
|  |     if !is_valid { | ||||||
|  |       warn!( | ||||||
|  |         "Invalid SAML auth data: username: {:?}, prelogin-cookie: {:?}, portal-userauthcookie: {:?}", | ||||||
|  |         username, prelogin_cookie, portal_userauthcookie | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     is_valid | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -84,3 +127,28 @@ pub fn parse_xml_tag(html: &str, tag: &str) -> Option<String> { | |||||||
|     .and_then(|captures| captures.get(1)) |     .and_then(|captures| captures.get(1)) | ||||||
|     .map(|m| m.as_str().to_string()) |     .map(|m| m.as_str().to_string()) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests { | ||||||
|  |   use super::*; | ||||||
|  |  | ||||||
|  |   #[test] | ||||||
|  |   fn auth_data_from_gpcallback_cas() { | ||||||
|  |     let auth_data = "globalprotectcallback:cas-as=1&un=xyz@email.com&token=very_long_string"; | ||||||
|  |  | ||||||
|  |     let auth_data = SamlAuthData::from_gpcallback(auth_data).unwrap(); | ||||||
|  |  | ||||||
|  |     assert_eq!(auth_data.username(), "xyz@email.com"); | ||||||
|  |     assert_eq!(auth_data.token(), Some("very_long_string")); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   #[test] | ||||||
|  |   fn auth_data_from_gpcallback_non_cas() { | ||||||
|  |     let auth_data = "PGh0bWw+PCEtLSA8c2FtbC1hdXRoLXN0YXR1cz4xPC9zYW1sLWF1dGgtc3RhdHVzPjxwcmVsb2dpbi1jb29raWU+cHJlbG9naW4tY29va2llPC9wcmVsb2dpbi1jb29raWU+PHNhbWwtdXNlcm5hbWU+eHl6QGVtYWlsLmNvbTwvc2FtbC11c2VybmFtZT48c2FtbC1zbG8+bm88L3NhbWwtc2xvPjxzYW1sLVNlc3Npb25Ob3RPbk9yQWZ0ZXI+PC9zYW1sLVNlc3Npb25Ob3RPbk9yQWZ0ZXI+IC0tPjwvaHRtbD4="; | ||||||
|  |  | ||||||
|  |     let auth_data = SamlAuthData::from_gpcallback(auth_data).unwrap(); | ||||||
|  |  | ||||||
|  |     assert_eq!(auth_data.username(), "xyz@email.com"); | ||||||
|  |     assert_eq!(auth_data.prelogin_cookie(), Some("prelogin-cookie")); | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,10 +1,9 @@ | |||||||
| use std::collections::HashMap; | use std::collections::HashMap; | ||||||
|  |  | ||||||
| use log::info; |  | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
| use specta::Type; | use specta::Type; | ||||||
|  |  | ||||||
| use crate::{auth::SamlAuthData, utils::base64::decode_to_string}; | use crate::auth::SamlAuthData; | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Deserialize, Type, Clone)] | #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||||
| #[serde(rename_all = "camelCase")] | #[serde(rename_all = "camelCase")] | ||||||
| @@ -40,14 +39,16 @@ impl From<&CachedCredential> for PasswordCredential { | |||||||
| #[serde(rename_all = "camelCase")] | #[serde(rename_all = "camelCase")] | ||||||
| pub struct PreloginCookieCredential { | pub struct PreloginCookieCredential { | ||||||
|   username: String, |   username: String, | ||||||
|   prelogin_cookie: String, |   prelogin_cookie: Option<String>, | ||||||
|  |   token: Option<String>, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl PreloginCookieCredential { | impl PreloginCookieCredential { | ||||||
|   pub fn new(username: &str, prelogin_cookie: &str) -> Self { |   pub fn new(username: &str, prelogin_cookie: Option<&str>, token: Option<&str>) -> Self { | ||||||
|     Self { |     Self { | ||||||
|       username: username.to_string(), |       username: username.to_string(), | ||||||
|       prelogin_cookie: prelogin_cookie.to_string(), |       prelogin_cookie: prelogin_cookie.map(|s| s.to_string()), | ||||||
|  |       token: token.map(|s| s.to_string()), | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -55,22 +56,22 @@ impl PreloginCookieCredential { | |||||||
|     &self.username |     &self.username | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   pub fn prelogin_cookie(&self) -> &str { |   pub fn prelogin_cookie(&self) -> Option<&str> { | ||||||
|     &self.prelogin_cookie |     self.prelogin_cookie.as_deref() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn token(&self) -> Option<&str> { | ||||||
|  |     self.token.as_deref() | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl TryFrom<SamlAuthData> for PreloginCookieCredential { | impl From<SamlAuthData> for PreloginCookieCredential { | ||||||
|   type Error = anyhow::Error; |   fn from(value: SamlAuthData) -> Self { | ||||||
|  |  | ||||||
|   fn try_from(value: SamlAuthData) -> Result<Self, Self::Error> { |  | ||||||
|     let username = value.username().to_string(); |     let username = value.username().to_string(); | ||||||
|     let prelogin_cookie = value |     let prelogin_cookie = value.prelogin_cookie(); | ||||||
|       .prelogin_cookie() |     let token = value.token(); | ||||||
|       .ok_or_else(|| anyhow::anyhow!("Missing prelogin cookie"))? |  | ||||||
|       .to_string(); |  | ||||||
|  |  | ||||||
|     Ok(Self::new(&username, &prelogin_cookie)) |     Self::new(&username, prelogin_cookie, token) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -155,31 +156,12 @@ impl From<PasswordCredential> for CachedCredential { | |||||||
|     ) |     ) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Deserialize, Type, Clone)] |  | ||||||
| pub struct TokenCredential { |  | ||||||
|   #[serde(alias = "un")] |  | ||||||
|   username: String, |  | ||||||
|   token: String, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl TokenCredential { |  | ||||||
|   pub fn username(&self) -> &str { |  | ||||||
|     &self.username |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   pub fn token(&self) -> &str { |  | ||||||
|     &self.token |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Deserialize, Type, Clone)] | #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||||
| #[serde(tag = "type", rename_all = "camelCase")] | #[serde(tag = "type", rename_all = "camelCase")] | ||||||
| pub enum Credential { | pub enum Credential { | ||||||
|   Password(PasswordCredential), |   Password(PasswordCredential), | ||||||
|   PreloginCookie(PreloginCookieCredential), |   PreloginCookie(PreloginCookieCredential), | ||||||
|   AuthCookie(AuthCookieCredential), |   AuthCookie(AuthCookieCredential), | ||||||
|   TokenCredential(TokenCredential), |  | ||||||
|   CachedCredential(CachedCredential), |   CachedCredential(CachedCredential), | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -187,19 +169,9 @@ impl Credential { | |||||||
|   /// Create a credential from a globalprotectcallback:<base64 encoded string>, |   /// Create a credential from a globalprotectcallback:<base64 encoded string>, | ||||||
|   /// or globalprotectcallback:cas-as=1&un=user@xyz.com&token=very_long_string |   /// or globalprotectcallback:cas-as=1&un=user@xyz.com&token=very_long_string | ||||||
|   pub fn from_gpcallback(auth_data: &str) -> anyhow::Result<Self> { |   pub fn from_gpcallback(auth_data: &str) -> anyhow::Result<Self> { | ||||||
|     let auth_data = auth_data.trim_start_matches("globalprotectcallback:"); |     let auth_data = SamlAuthData::from_gpcallback(auth_data)?; | ||||||
|  |  | ||||||
|     if auth_data.starts_with("cas-as") { |     Ok(Self::from(auth_data)) | ||||||
|       info!("Got token auth data: {}", auth_data); |  | ||||||
|       let token_cred: TokenCredential = serde_urlencoded::from_str(auth_data)?; |  | ||||||
|       Ok(Self::TokenCredential(token_cred)) |  | ||||||
|     } else { |  | ||||||
|       info!("Parsing SAML auth data..."); |  | ||||||
|       let auth_data = decode_to_string(auth_data)?; |  | ||||||
|       let auth_data = SamlAuthData::from_html(&auth_data)?; |  | ||||||
|  |  | ||||||
|       Self::try_from(auth_data) |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   pub fn username(&self) -> &str { |   pub fn username(&self) -> &str { | ||||||
| @@ -207,7 +179,6 @@ impl Credential { | |||||||
|       Credential::Password(cred) => cred.username(), |       Credential::Password(cred) => cred.username(), | ||||||
|       Credential::PreloginCookie(cred) => cred.username(), |       Credential::PreloginCookie(cred) => cred.username(), | ||||||
|       Credential::AuthCookie(cred) => cred.username(), |       Credential::AuthCookie(cred) => cred.username(), | ||||||
|       Credential::TokenCredential(cred) => cred.username(), |  | ||||||
|       Credential::CachedCredential(cred) => cred.username(), |       Credential::CachedCredential(cred) => cred.username(), | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -218,7 +189,7 @@ impl Credential { | |||||||
|  |  | ||||||
|     let (passwd, prelogin_cookie, portal_userauthcookie, portal_prelogonuserauthcookie, token) = match self { |     let (passwd, prelogin_cookie, portal_userauthcookie, portal_prelogonuserauthcookie, token) = match self { | ||||||
|       Credential::Password(cred) => (Some(cred.password()), None, None, None, None), |       Credential::Password(cred) => (Some(cred.password()), None, None, None, None), | ||||||
|       Credential::PreloginCookie(cred) => (None, Some(cred.prelogin_cookie()), None, None, None), |       Credential::PreloginCookie(cred) => (None, cred.prelogin_cookie(), None, None, cred.token()), | ||||||
|       Credential::AuthCookie(cred) => ( |       Credential::AuthCookie(cred) => ( | ||||||
|         None, |         None, | ||||||
|         None, |         None, | ||||||
| @@ -226,7 +197,6 @@ impl Credential { | |||||||
|         Some(cred.prelogon_user_auth_cookie()), |         Some(cred.prelogon_user_auth_cookie()), | ||||||
|         None, |         None, | ||||||
|       ), |       ), | ||||||
|       Credential::TokenCredential(cred) => (None, None, None, None, Some(cred.token())), |  | ||||||
|       Credential::CachedCredential(cred) => ( |       Credential::CachedCredential(cred) => ( | ||||||
|         cred.password(), |         cred.password(), | ||||||
|         None, |         None, | ||||||
| @@ -252,13 +222,11 @@ impl Credential { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl TryFrom<SamlAuthData> for Credential { | impl From<SamlAuthData> for Credential { | ||||||
|   type Error = anyhow::Error; |   fn from(value: SamlAuthData) -> Self { | ||||||
|  |     let cred = PreloginCookieCredential::from(value); | ||||||
|  |  | ||||||
|   fn try_from(value: SamlAuthData) -> Result<Self, Self::Error> { |     Self::PreloginCookie(cred) | ||||||
|     let prelogin_cookie = PreloginCookieCredential::try_from(value)?; |  | ||||||
|  |  | ||||||
|     Ok(Self::PreloginCookie(prelogin_cookie)) |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -279,38 +247,3 @@ impl From<&CachedCredential> for Credential { | |||||||
|     Self::CachedCredential(value.clone()) |     Self::CachedCredential(value.clone()) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| #[cfg(test)] |  | ||||||
| mod tests { |  | ||||||
|   use super::*; |  | ||||||
|  |  | ||||||
|   #[test] |  | ||||||
|   fn cred_from_gpcallback_cas() { |  | ||||||
|     let auth_data = "globalprotectcallback:cas-as=1&un=xyz@email.com&token=very_long_string"; |  | ||||||
|  |  | ||||||
|     let cred = Credential::from_gpcallback(auth_data).unwrap(); |  | ||||||
|  |  | ||||||
|     match cred { |  | ||||||
|       Credential::TokenCredential(token_cred) => { |  | ||||||
|         assert_eq!(token_cred.username(), "xyz@email.com"); |  | ||||||
|         assert_eq!(token_cred.token(), "very_long_string"); |  | ||||||
|       } |  | ||||||
|       _ => panic!("Expected TokenCredential"), |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   #[test] |  | ||||||
|   fn cred_from_gpcallback_non_cas() { |  | ||||||
|     let auth_data = "PGh0bWw+PCEtLSA8c2FtbC1hdXRoLXN0YXR1cz4xPC9zYW1sLWF1dGgtc3RhdHVzPjxwcmVsb2dpbi1jb29raWU+cHJlbG9naW4tY29va2llPC9wcmVsb2dpbi1jb29raWU+PHNhbWwtdXNlcm5hbWU+eHl6QGVtYWlsLmNvbTwvc2FtbC11c2VybmFtZT48c2FtbC1zbG8+bm88L3NhbWwtc2xvPjxzYW1sLVNlc3Npb25Ob3RPbk9yQWZ0ZXI+PC9zYW1sLVNlc3Npb25Ob3RPbk9yQWZ0ZXI+IC0tPjwvaHRtbD4="; |  | ||||||
|  |  | ||||||
|     let cred = Credential::from_gpcallback(auth_data).unwrap(); |  | ||||||
|  |  | ||||||
|     match cred { |  | ||||||
|       Credential::PreloginCookie(cred) => { |  | ||||||
|         assert_eq!(cred.username(), "xyz@email.com"); |  | ||||||
|         assert_eq!(cred.prelogin_cookie(), "prelogin-cookie"); |  | ||||||
|       } |  | ||||||
|       _ => panic!("Expected PreloginCookieCredential") |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -9,3 +9,11 @@ pub enum PortalError { | |||||||
|   #[error("Network error: {0}")] |   #[error("Network error: {0}")] | ||||||
|   NetworkError(String), |   NetworkError(String), | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[derive(Error, Debug)] | ||||||
|  | pub enum AuthDataParseError { | ||||||
|  |   #[error("No auth data found")] | ||||||
|  |   NotFound, | ||||||
|  |   #[error("Invalid auth data")] | ||||||
|  |   Invalid, | ||||||
|  | } | ||||||
|   | |||||||
| @@ -51,7 +51,6 @@ pub struct GpParams { | |||||||
|   client_version: Option<String>, |   client_version: Option<String>, | ||||||
|   computer: String, |   computer: String, | ||||||
|   ignore_tls_errors: bool, |   ignore_tls_errors: bool, | ||||||
|   prefer_default_browser: bool, |  | ||||||
| } | } | ||||||
|  |  | ||||||
| impl GpParams { | impl GpParams { | ||||||
| @@ -79,14 +78,6 @@ impl GpParams { | |||||||
|     self.ignore_tls_errors |     self.ignore_tls_errors | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   pub fn prefer_default_browser(&self) -> bool { |  | ||||||
|     self.prefer_default_browser |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   pub fn set_prefer_default_browser(&mut self, prefer_default_browser: bool) { |  | ||||||
|     self.prefer_default_browser = prefer_default_browser; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   pub fn client_os(&self) -> &str { |   pub fn client_os(&self) -> &str { | ||||||
|     self.client_os.as_str() |     self.client_os.as_str() | ||||||
|   } |   } | ||||||
| @@ -135,7 +126,6 @@ pub struct GpParamsBuilder { | |||||||
|   client_version: Option<String>, |   client_version: Option<String>, | ||||||
|   computer: String, |   computer: String, | ||||||
|   ignore_tls_errors: bool, |   ignore_tls_errors: bool, | ||||||
|   prefer_default_browser: bool, |  | ||||||
| } | } | ||||||
|  |  | ||||||
| impl GpParamsBuilder { | impl GpParamsBuilder { | ||||||
| @@ -148,7 +138,6 @@ impl GpParamsBuilder { | |||||||
|       client_version: Default::default(), |       client_version: Default::default(), | ||||||
|       computer: whoami::hostname(), |       computer: whoami::hostname(), | ||||||
|       ignore_tls_errors: false, |       ignore_tls_errors: false, | ||||||
|       prefer_default_browser: false, |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -187,11 +176,6 @@ impl GpParamsBuilder { | |||||||
|     self |     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 { |   pub fn build(&self) -> GpParams { | ||||||
|     GpParams { |     GpParams { | ||||||
|       is_gateway: self.is_gateway, |       is_gateway: self.is_gateway, | ||||||
| @@ -201,7 +185,6 @@ impl GpParamsBuilder { | |||||||
|       client_version: self.client_version.clone(), |       client_version: self.client_version.clone(), | ||||||
|       computer: self.computer.clone(), |       computer: self.computer.clone(), | ||||||
|       ignore_tls_errors: self.ignore_tls_errors, |       ignore_tls_errors: self.ignore_tls_errors, | ||||||
|       prefer_default_browser: self.prefer_default_browser, |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -29,7 +29,6 @@ pub struct SamlPrelogin { | |||||||
|   is_gateway: bool, |   is_gateway: bool, | ||||||
|   saml_request: String, |   saml_request: String, | ||||||
|   support_default_browser: bool, |   support_default_browser: bool, | ||||||
|   is_cas: bool, |  | ||||||
| } | } | ||||||
|  |  | ||||||
| impl SamlPrelogin { | impl SamlPrelogin { | ||||||
| @@ -44,14 +43,6 @@ impl SamlPrelogin { | |||||||
|   pub fn support_default_browser(&self) -> bool { |   pub fn support_default_browser(&self) -> bool { | ||||||
|     self.support_default_browser |     self.support_default_browser | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   pub fn is_cas(&self) -> bool { |  | ||||||
|     self.is_cas |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   fn set_is_cas(&mut self, is_cas: bool) { |  | ||||||
|     self.is_cas = is_cas; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Type, Clone)] | #[derive(Debug, Serialize, Type, Clone)] | ||||||
| @@ -106,29 +97,6 @@ impl Prelogin { | |||||||
| } | } | ||||||
|  |  | ||||||
| pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prelogin> { | pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prelogin> { | ||||||
|   match prelogin_impl(portal, gp_params).await { |  | ||||||
|     Ok(prelogin) => Ok(prelogin), |  | ||||||
|     Err(e) => { |  | ||||||
|       if e.to_string().contains("CAS is not supported by the client") { |  | ||||||
|         info!("CAS authentication detected, retrying with default browser"); |  | ||||||
|         let mut gp_params = gp_params.clone(); |  | ||||||
|         gp_params.set_prefer_default_browser(true); |  | ||||||
|  |  | ||||||
|         let mut prelogin = prelogin_impl(portal, &gp_params).await?; |  | ||||||
|         // Mark the prelogin as CAS |  | ||||||
|         if let Prelogin::Saml(saml) = &mut prelogin { |  | ||||||
|           saml.set_is_cas(true); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         Ok(prelogin) |  | ||||||
|       } else { |  | ||||||
|         Err(e) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| pub async fn prelogin_impl(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prelogin> { |  | ||||||
|   let user_agent = gp_params.user_agent(); |   let user_agent = gp_params.user_agent(); | ||||||
|   info!("Prelogin with user_agent: {}", user_agent); |   info!("Prelogin with user_agent: {}", user_agent); | ||||||
|  |  | ||||||
| @@ -139,11 +107,8 @@ pub async fn prelogin_impl(portal: &str, gp_params: &GpParams) -> anyhow::Result | |||||||
|   let mut params = gp_params.to_params(); |   let mut params = gp_params.to_params(); | ||||||
|  |  | ||||||
|   params.insert("tmp", "tmp"); |   params.insert("tmp", "tmp"); | ||||||
|   // CAS support requires external browser |  | ||||||
|   if gp_params.prefer_default_browser() { |  | ||||||
|   params.insert("default-browser", "1"); |   params.insert("default-browser", "1"); | ||||||
|   params.insert("cas-support", "yes"); |   params.insert("cas-support", "yes"); | ||||||
|   } |  | ||||||
|  |  | ||||||
|   params.retain(|k, _| REQUIRED_PARAMS.iter().any(|required_param| required_param == k)); |   params.retain(|k, _| REQUIRED_PARAMS.iter().any(|required_param| required_param == k)); | ||||||
|  |  | ||||||
| @@ -213,7 +178,6 @@ fn parse_res_xml(res_xml: String, is_gateway: bool) -> anyhow::Result<Prelogin> | |||||||
|       is_gateway, |       is_gateway, | ||||||
|       saml_request, |       saml_request, | ||||||
|       support_default_browser, |       support_default_browser, | ||||||
|       is_cas: false, |  | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     return Ok(Prelogin::Saml(saml_prelogin)); |     return Ok(Prelogin::Saml(saml_prelogin)); | ||||||
|   | |||||||
| @@ -135,7 +135,7 @@ impl<'a> SamlAuthLauncher<'a> { | |||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     match auth_result { |     match auth_result { | ||||||
|       SamlAuthResult::Success(auth_data) => Credential::try_from(auth_data), |       SamlAuthResult::Success(auth_data) => Ok(Credential::from(auth_data)), | ||||||
|       SamlAuthResult::Failure(msg) => bail!(msg), |       SamlAuthResult::Failure(msg) => bail!(msg), | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user