Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion crates/defguard_common/src/db/models/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use url::Url;
use utoipa::ToSchema;
use uuid::Uuid;

use crate::{db::Id, global_value, secret::SecretStringWrapper};
use crate::{db::Id, global_value, secret::SecretStringWrapper, types::AuthFlowType};

global_value!(SETTINGS, Option<Settings>, None, set_settings, get_settings);

Expand Down Expand Up @@ -529,6 +529,19 @@ impl Settings {
pub fn proxy_public_url(&self) -> Result<Url, url::ParseError> {
Url::parse(&self.public_proxy_url)
}

/// Returns configured Edge Component URL with the correct callback path appended depending on auth flow type.
pub fn edge_callback_url(&self, auth_flow_type: AuthFlowType) -> Result<Url, url::ParseError> {
let mut url = self.proxy_public_url()?;
// Append callback segments to the URL.
if let Ok(mut path_segments) = url.path_segments_mut() {
match auth_flow_type {
AuthFlowType::Enrollment => path_segments.extend(&["openid", "callback"]),
AuthFlowType::Mfa => path_segments.extend(&["openid", "mfa", "callback"]),
};
}
Ok(url)
}
}

#[derive(Serialize)]
Expand Down Expand Up @@ -676,4 +689,35 @@ mod test {
"https://defguard.example.com:8443/path/auth/callback"
);
}

#[test]
fn test_edge_callback_url() {
let mut s = Settings {
public_proxy_url: "https://edge.example.com".into(),
..Default::default()
};

assert_eq!(
s.edge_callback_url(AuthFlowType::Enrollment)
.unwrap()
.as_str(),
"https://edge.example.com/openid/callback"
);
assert_eq!(
s.edge_callback_url(AuthFlowType::Mfa).unwrap().as_str(),
"https://edge.example.com/openid/mfa/callback"
);

s.public_proxy_url = "https://edge.example.com:8443/path".into();
assert_eq!(
s.edge_callback_url(AuthFlowType::Enrollment)
.unwrap()
.as_str(),
"https://edge.example.com:8443/path/openid/callback"
);
assert_eq!(
s.edge_callback_url(AuthFlowType::Mfa).unwrap().as_str(),
"https://edge.example.com:8443/path/openid/mfa/callback"
);
}
}
6 changes: 6 additions & 0 deletions crates/defguard_common/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ pub mod proxy;
pub mod user_info;

pub type UrlParseError = url::ParseError;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AuthFlowType {
Enrollment,
Mfa,
}
14 changes: 8 additions & 6 deletions crates/defguard_core/src/enterprise/grpc/desktop_client_mfa.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use defguard_common::{db::models::Settings, types::AuthFlowType};
use defguard_proto::proxy::{ClientMfaOidcAuthenticateRequest, DeviceInfo, MfaMethod};
use openidconnect::{AuthorizationCode, Nonce};
use reqwest::Url;
use tonic::Status;

use crate::{
Expand Down Expand Up @@ -84,10 +84,12 @@ impl ClientMfaServer {
);

let code = AuthorizationCode::new(request.code.clone());
let url = match Url::parse(&request.callback_url).map_err(|err| {
error!("Invalid redirect URL provided: {err}");
Status::invalid_argument("invalid redirect URL")
}) {
let url = match Settings::get_current_settings()
.edge_callback_url(AuthFlowType::Mfa)
.map_err(|err| {
error!("Invalid callback URL configuration: {err}");
Status::invalid_argument("invalid callback URL")
}) {
Ok(url) => url,
Err(status) => {
self.sessions
Expand All @@ -101,7 +103,7 @@ impl ClientMfaServer {
location: location.clone(),
device: device.clone(),
method,
message: "provided invalid redirect URL".to_string(),
message: "provided invalid callback URL".to_string(),
},
)),
})?;
Expand Down
136 changes: 78 additions & 58 deletions crates/defguard_proxy_manager/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use defguard_common::{
Id,
models::{Settings, proxy::Proxy},
},
types::AuthFlowType,
};
use defguard_core::{
db::models::enrollment::{ENROLLMENT_TOKEN_TYPE, Token},
Expand All @@ -34,8 +35,8 @@ use defguard_core::{
};
use defguard_grpc_tls::{certs as tls_certs, connector::HttpsSchemeConnector};
use defguard_proto::proxy::{
AuthCallbackResponse, AuthInfoResponse, CoreError, CoreRequest, CoreResponse, InitialInfo,
core_request, core_response, proxy_client::ProxyClient,
AuthCallbackResponse, AuthFlowType as ProtoAuthFlowType, AuthInfoResponse, CoreError,
CoreRequest, CoreResponse, InitialInfo, core_request, core_response, proxy_client::ProxyClient,
};
use defguard_version::{
ComponentInfo, DefguardComponent, client::ClientVersionInterceptor, get_tracing_variables,
Expand Down Expand Up @@ -624,70 +625,90 @@ impl ProxyHandler {
status_code: Code::FailedPrecondition as i32,
message: "no valid license".into(),
}))
} else if let Ok(redirect_url) = Url::parse(&request.redirect_url) {
if let Some(provider) = OpenIdProvider::get_current(&pool).await? {
match make_oidc_client(redirect_url, &provider).await {
Ok((_client_id, client)) => {
let mut authorize_url_builder = client
.authorize_url(
CoreAuthenticationFlow::AuthorizationCode,
|| build_state(request.state),
Nonce::new_random,
)
.add_scope(Scope::new("email".to_string()))
.add_scope(Scope::new("profile".to_string()));

if SELECT_ACCOUNT_SUPPORTED_PROVIDERS
.iter()
.all(|p| p.eq_ignore_ascii_case(&provider.name))
{
authorize_url_builder = authorize_url_builder
.add_prompt(
openidconnect::core::CoreAuthPrompt::SelectAccount,
);
} else {
let redirect_url = match request.auth_flow_type() {
ProtoAuthFlowType::Enrollment => {
let settings = Settings::get_current_settings();
settings.edge_callback_url(AuthFlowType::Enrollment)
}
ProtoAuthFlowType::Mfa => {
let settings = Settings::get_current_settings();
settings.edge_callback_url(AuthFlowType::Mfa)
}
// fall back for legacy pre-2.0 clients
ProtoAuthFlowType::Unspecified =>
{
#[allow(deprecated)]
Url::parse(&request.redirect_url)
}
};

if let Ok(redirect_url) = redirect_url {
if let Some(provider) =
OpenIdProvider::get_current(&pool).await?
{
match make_oidc_client(redirect_url, &provider).await {
Ok((_client_id, client)) => {
let mut authorize_url_builder = client
.authorize_url(
CoreAuthenticationFlow::AuthorizationCode,
|| build_state(request.state),
Nonce::new_random,
)
.add_scope(Scope::new("email".to_string()))
.add_scope(Scope::new("profile".to_string()));

if SELECT_ACCOUNT_SUPPORTED_PROVIDERS
.iter()
.all(|p| p.eq_ignore_ascii_case(&provider.name))
{
authorize_url_builder = authorize_url_builder
.add_prompt(
openidconnect::core::CoreAuthPrompt::SelectAccount,
);
}
let (url, csrf_token, nonce) =
authorize_url_builder.url();

Some(core_response::Payload::AuthInfo(
AuthInfoResponse {
url: url.into(),
csrf_token: csrf_token.secret().to_owned(),
nonce: nonce.secret().to_owned(),
button_display_name: provider.display_name,
},
))
}
Err(err) => {
error!(
"Failed to setup external OIDC provider client: {err}"
);
Some(core_response::Payload::CoreError(CoreError {
status_code: Code::Internal as i32,
message: "failed to build OIDC client".into(),
}))
}
let (url, csrf_token, nonce) =
authorize_url_builder.url();

Some(core_response::Payload::AuthInfo(
AuthInfoResponse {
url: url.into(),
csrf_token: csrf_token.secret().to_owned(),
nonce: nonce.secret().to_owned(),
button_display_name: provider.display_name,
},
))
}
Err(err) => {
error!(
"Failed to setup external OIDC provider client: {err}"
);
Some(core_response::Payload::CoreError(CoreError {
status_code: Code::Internal as i32,
message: "failed to build OIDC client".into(),
}))
}
} else {
error!("Failed to get current OpenID provider");
Some(core_response::Payload::CoreError(CoreError {
status_code: Code::NotFound as i32,
message: "failed to get current OpenID provider".into(),
}))
}
} else {
error!("Failed to get current OpenID provider");
error!("Invalid redirect URL in authentication info request");
Some(core_response::Payload::CoreError(CoreError {
status_code: Code::NotFound as i32,
message: "failed to get current OpenID provider".into(),
status_code: Code::Internal as i32,
message: "invalid redirect URL".into(),
}))
}
} else {
error!(
"Invalid redirect URL in authentication info request: {}",
request.redirect_url
);
Some(core_response::Payload::CoreError(CoreError {
status_code: Code::Internal as i32,
message: "invalid redirect URL".into(),
}))
}
}
Some(core_request::Payload::AuthCallback(request)) => {
match Url::parse(&request.callback_url) {
match Settings::get_current_settings()
.edge_callback_url(AuthFlowType::Enrollment)
{
Ok(callback_url) => {
let code = AuthorizationCode::new(request.code);
match user_from_claims(
Expand Down Expand Up @@ -759,8 +780,7 @@ impl ProxyHandler {
Err(err) => {
error!(
"Proxy requested an OpenID authentication info for a callback \
URL ({}) that couldn't be parsed. Details: {err}",
request.callback_url
URL that couldn't be built. Details: {err}"
);
Some(core_response::Payload::CoreError(CoreError {
status_code: Code::Internal as i32,
Expand Down
2 changes: 1 addition & 1 deletion proto
Submodule proto updated 1 files
+13 −3 core/proxy.proto
19 changes: 10 additions & 9 deletions web/messages/en/edge.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"edge_title": "Edge components",
"edge_edit_title": "Edit edge component",
"edge_title": "Edge Components",
"edge_edit_title": "Edit Edge Component",
"edge_edit_general_info": "General information",
"edge_edit_name": "Name",
"edge_edit_address": "IP or Domain",
"edge_edit_port": "gRPC port",
"edge_edit_public_address": "Public domain",
"edge_edit_delete": "Delete",
"edge_edit_success": "Edge component updated",
"edge_edit_failed": "Failed to update edge component",
"edge_edit_success": "Edge Component updated",
"edge_edit_failed": "Failed to update Edge Component",
"edges_header_title": "All components",
"edges_col_name": "Name",
"edges_col_address": "Address",
Expand All @@ -19,11 +19,12 @@
"edges_col_modified_by": "Modified by",
"edges_col_status": "Status",
"edges_row_menu_edit": "Edit",
"edges_empty_title": "No edge components added yet.",
"edges_empty_subtitle": "Add edge components by clicking the button below.",
"edges_empty_title": "No Edge Components added yet.",
"edges_empty_subtitle": "Add Edge Components by clicking the button below.",
"edges_search_placeholder": "Search",
"edge_delete_success": "Edge component deleted",
"edge_delete_failed": "Failed to delete edge component",
"edge_delete_success": "Edge Component deleted",
"edge_delete_failed": "Failed to delete Edge Component",
"edge_connected": "Connected",
"edge_disconnected": "Disconnected"
"edge_disconnected": "Disconnected",
"edge_add": "Add Edge Component"
}
18 changes: 17 additions & 1 deletion web/messages/en/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"settings_page_title": "Settings",
"settings_breadcrumb_general": "General",
"settings_breadcrumb_instance": "Instance settings",
"settings_instance_title": "Instance settings",
"settings_instance_subtitle": "Here you can configure general instance parameters.",
"settings_instance_label_name": "Instance name",
"settings_instance_label_public_proxy_url": "Public Edge Component URL",
"settings_instance_label_session_duration": "Session duration",
"settings_instance_session_duration_1": "1 day",
"settings_instance_session_duration_2": "2 days",
"settings_instance_session_duration_3": "3 days",
"settings_instance_session_duration_7": "7 days",
"settings_instance_session_duration_10": "10 days",
"settings_instance_session_duration_14": "14 days",
"settings_instance_session_duration_30": "30 days",
"settings_activity_log_streaming_title": "Activity log streaming",
"settings_activity_log_streaming_description": "Monitor and export real-time activity logs from your Defguard instance. Stream events to external systems for auditing, analytics, or security monitoring.",
"settings_activity_log_streaming_no_upstreams": "You don't have any activity log upstreams.",
Expand All @@ -9,5 +24,6 @@
"settings_activity_log_streaming_table_title": "All log streams",
"settings_activity_log_streaming_table_header_name": "Name",
"settings_activity_log_streaming_table_stream_type_name": "Destination",
"settings_msg_saved": "Settings saved"
"settings_msg_saved": "Settings saved",
"settings_msg_save_failed": "Failed to save settings"
}
2 changes: 1 addition & 1 deletion web/src/pages/EdgesPage/EdgesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export const EdgesTable = () => {
const addButtonProps = useMemo(
(): ButtonProps => ({
variant: 'primary',
text: 'Add Edge component',
text: m.edge_add(),
iconLeft: 'globe',
testId: 'add-edge',
onClick: () => {
Expand Down
Loading
Loading