diff --git a/src/cli.rs b/src/cli.rs index 97e71e547..1e44d36bd 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -631,12 +631,23 @@ pub struct Options { long = "oidc-scope", name = "oidc-scope", env = "P_OIDC_SCOPE", - default_value = "openid profile email offline_access", + default_value = "openid profile email", required = false, - help = "OIDC scope to request (default: openid profile email offline_access)" + help = "OIDC scope to request (default: openid profile email)" )] pub scope: String, + // additional query params for oidc auth url + #[arg( + long, + name = "oidc-query-params", + env = "P_OIDC_QUERY_PARAMS", + value_parser = validation::validate_query_params, + required = false, + help = "Additional query params to add to the auth url" + )] + pub oidc_query_params: Option, + // event's maximum chunk age in hours #[arg( long, diff --git a/src/handlers/http/oidc.rs b/src/handlers/http/oidc.rs index b822db408..1ce09c69e 100644 --- a/src/handlers/http/oidc.rs +++ b/src/handlers/http/oidc.rs @@ -96,7 +96,13 @@ pub async fn login( let scope = PARSEABLE.options.scope.to_string(); let mut auth_url: String = client.read().await.auth_url(&scope, Some(redirect)).into(); - auth_url.push_str("&access_type=offline"); + if let Some(query_params) = PARSEABLE.options.oidc_query_params.as_ref() { + if !query_params.starts_with('&') { + auth_url = format!("{auth_url}&{query_params}"); + } else { + auth_url.push_str(query_params.as_str()); + } + } return Ok(HttpResponse::TemporaryRedirect() .insert_header((actix_web::http::header::LOCATION, auth_url)) .finish()); @@ -153,7 +159,13 @@ pub async fn login( .await .auth_url(&scope, Some(redirect)) .into(); - auth_url.push_str("&access_type=offline"); + if let Some(query_params) = PARSEABLE.options.oidc_query_params.as_ref() { + if !query_params.starts_with('&') { + auth_url = format!("{auth_url}&{query_params}"); + } else { + auth_url.push_str(query_params.as_str()); + } + } HttpResponse::TemporaryRedirect() .insert_header((actix_web::http::header::LOCATION, auth_url)) .finish() diff --git a/src/option.rs b/src/option.rs index 1485bcd30..63023d226 100644 --- a/src/option.rs +++ b/src/option.rs @@ -209,6 +209,56 @@ pub mod validation { } } + pub fn validate_query_params(mut query: &str) -> Result { + fn valid_component(value: &str) -> bool { + let bytes = value.as_bytes(); + let mut i = 0; + + while i < bytes.len() { + match bytes[i] { + b'%' if i + 2 < bytes.len() + && bytes[i + 1].is_ascii_hexdigit() + && bytes[i + 2].is_ascii_hexdigit() => + { + i += 3; + } + b if b.is_ascii_alphanumeric() + || matches!(b, b'-' | b'.' | b'_' | b'~' | b'+') => + { + i += 1; + } + _ => return false, + } + } + + true + } + + if query.is_empty() { + return Err("query string cannot be empty".into()); + } + query = if let Some(query) = query.strip_prefix('&') { + query + } else { + query + }; + for param in query.split('&') { + let (key, value) = param + .split_once('=') + .ok_or_else(|| format!("invalid parameter: {param}"))?; + + if key.is_empty() { + return Err("parameter key cannot be empty".into()); + } + + if !valid_component(key) || !valid_component(value) { + return Err(format!("invalid characters in parameter: {param}")); + } + } + + Ok(query.to_owned()) + } + pub fn validate_payload_size(s: &str) -> Result { const MIN_SIZE: usize = 100; // 100 bytes const MAX_SIZE: usize = 100 * 1024 * 1024; // 100 MB