Skip to content

Commit

Permalink
Support SSO using default browser
Browse files Browse the repository at this point in the history
  • Loading branch information
yuezk committed Jan 22, 2024
1 parent 0b55a80 commit c3bd7ae
Show file tree
Hide file tree
Showing 16 changed files with 201 additions and 36 deletions.
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
"dotenvy",
"getconfig",
"globalprotect",
"globalprotectcallback",
"gpapi",
"gpauth",
"gpcallback",
"gpclient",
"gpcommon",
"gpgui",
Expand Down Expand Up @@ -50,5 +52,6 @@
"wmctrl",
"XAUTHORITY",
"yuezk"
]
],
"rust-analyzer.cargo.features": "all",
}
47 changes: 42 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ resolver = "2"
members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"]

[workspace.package]
version = "2.0.0-beta4"
version = "2.0.0-beta5"
authors = ["Kevin Yue <k3vinyue@gmail.com>"]
homepage = "https://github.com/yuezk/GlobalProtect-openconnect"
edition = "2021"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ A GUI for GlobalProtect VPN, based on OpenConnect, supports the SSO authenticati
- [x] Better Linux support
- [x] Support both CLI and GUI
- [x] Support both SSO and non-SSO authentication
- [x] Support authentication using default browser
- [x] Support multiple portals
- [x] Support gateway selection
- [x] Support auto-connect on startup
Expand Down
26 changes: 25 additions & 1 deletion apps/gpclient/src/launch_gui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ use log::info;

#[derive(Args)]
pub(crate) struct LaunchGuiArgs {
#[clap(long, help = "Launch the GUI minimized")]
#[arg(
required = false,
help = "The authentication data, used for the default browser authentication"
)]
auth_data: Option<String>,
#[arg(long, help = "Launch the GUI minimized")]
minimized: bool,
}

Expand All @@ -30,6 +35,12 @@ impl<'a> LaunchGuiHandler<'a> {
anyhow::bail!("`launch-gui` cannot be run as root");
}

let auth_data = self.args.auth_data.as_deref().unwrap_or_default();
if !auth_data.is_empty() {
// Process the authentication data, its format is `globalprotectcallback:<data>`
return feed_auth_data(auth_data).await;
}

if try_active_gui().await.is_ok() {
info!("The GUI is already running");
return Ok(());
Expand Down Expand Up @@ -66,6 +77,19 @@ impl<'a> LaunchGuiHandler<'a> {
}
}

async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> {
let service_endpoint = http_endpoint().await?;

reqwest::Client::default()
.post(format!("{}/auth-data", service_endpoint))
.json(&auth_data)
.send()
.await?
.error_for_status()?;

Ok(())
}

async fn try_active_gui() -> anyhow::Result<()> {
let service_endpoint = http_endpoint().await?;

Expand Down
19 changes: 0 additions & 19 deletions apps/gpservice/com.yuezk.gpservice.policy

This file was deleted.

7 changes: 7 additions & 0 deletions apps/gpservice/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ pub(crate) async fn active_gui(State(ctx): State<Arc<WsServerContext>>) -> impl
ctx.send_event(WsEvent::ActiveGui).await;
}

pub(crate) async fn auth_data(
State(ctx): State<Arc<WsServerContext>>,
body: String,
) -> impl IntoResponse {
ctx.send_event(WsEvent::AuthData(body)).await;
}

pub(crate) async fn ws_handler(
ws: WebSocketUpgrade,
State(ctx): State<Arc<WsServerContext>>,
Expand Down
1 change: 1 addition & 0 deletions apps/gpservice/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub(crate) fn routes(ctx: Arc<WsServerContext>) -> Router {
Router::new()
.route("/health", get(handlers::health))
.route("/active-gui", post(handlers::active_gui))
.route("/auth-data", post(handlers::auth_data))
.route("/ws", get(handlers::ws_handler))
.with_state(ctx)
}
2 changes: 2 additions & 0 deletions crates/gpapi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
35 changes: 35 additions & 0 deletions crates/gpapi/src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use anyhow::bail;
use regex::Regex;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -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
}
Expand All @@ -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())
}
13 changes: 12 additions & 1 deletion crates/gpapi/src/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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(),
Expand Down
20 changes: 17 additions & 3 deletions crates/gpapi/src/gp_params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub struct GpParams {
client_version: Option<String>,
computer: String,
ignore_tls_errors: bool,
prefer_default_browser: bool,
}

impl GpParams {
Expand All @@ -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();
Expand All @@ -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
}
Expand All @@ -103,6 +109,7 @@ pub struct GpParamsBuilder {
client_version: Option<String>,
computer: String,
ignore_tls_errors: bool,
prefer_default_browser: bool,
}

impl GpParamsBuilder {
Expand All @@ -114,6 +121,7 @@ impl GpParamsBuilder {
client_version: Default::default(),
computer: whoami::hostname(),
ignore_tls_errors: false,
prefer_default_browser: false,
}
}

Expand Down Expand Up @@ -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(),
Expand All @@ -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,
}
}
}
Expand Down
Loading

0 comments on commit c3bd7ae

Please sign in to comment.